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
|
|
@ -0,0 +1,194 @@
|
|||
define(['require','exports','module','db','ext/co','core','ext/threads','provider/factory','store/factory'],function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var Db = require('db');
|
||||
var co = require('ext/co');
|
||||
var core = require('core');
|
||||
//var object = require('common/object');
|
||||
var threads = require('ext/threads');
|
||||
|
||||
var service = threads.service('calendar');
|
||||
var loadDb;
|
||||
|
||||
core.db = new Db('b2g-calendar');
|
||||
core.providerFactory = require('provider/factory');
|
||||
core.storeFactory = require('store/factory');
|
||||
|
||||
function start() {
|
||||
if (loadDb != null) {
|
||||
return loadDb;
|
||||
}
|
||||
|
||||
loadDb = core.db.load();
|
||||
return loadDb;
|
||||
}
|
||||
|
||||
function method(endpoint, handler) {
|
||||
service.method(endpoint, (...args) => {
|
||||
return co(function *() {
|
||||
yield start();
|
||||
return handler.apply(null, args);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stream(endpoint, handler) {
|
||||
service.stream(endpoint, function *() {
|
||||
var args = Array.slice(arguments);
|
||||
yield start();
|
||||
handler.apply(null, args);
|
||||
});
|
||||
}
|
||||
|
||||
function echo() {
|
||||
return Array.slice(arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will not work until the sync controller
|
||||
* (1) is not a class anymore
|
||||
* (2) doesn't need the app ns/object
|
||||
*/
|
||||
function saveAccount(details) {
|
||||
return co(function *() {
|
||||
var store = core.storeFactory.get('Account');
|
||||
var account;
|
||||
try {
|
||||
account = yield store.verifyAndPersist(details);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
yield syncAccount(account);
|
||||
return account;
|
||||
});
|
||||
}
|
||||
|
||||
function removeAccount(id) {
|
||||
return co(function *() {
|
||||
var store = core.storeFactory.get('Account');
|
||||
yield store.remove(id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This will not work until the sync controller
|
||||
* (1) is not a class anymore
|
||||
* (2) doesn't need the app ns/object
|
||||
*/
|
||||
function syncAccount(account) {
|
||||
console.log('syncAccount', account);
|
||||
/*
|
||||
return co(function *() {
|
||||
var store = core.storeFactory.get('Calendar');
|
||||
var calendars;
|
||||
try {
|
||||
calendars = yield store.remotesByAccount(account._id);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
var syncCalendar = syncController.calendar.bind(null, account);
|
||||
yield Promise.all(object.map(calendars, syncCalendar));
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
function saveEvent(exists, event) {
|
||||
return co(function *() {
|
||||
var store = core.storeFactory.get('Event');
|
||||
var provider = yield store.providerFor(event);
|
||||
var capabilities;
|
||||
try {
|
||||
capabilities = yield provider.eventCapabilities(event.data);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
var capability = exists ? 'canUpdate' : 'canCreate';
|
||||
if (!capabilities[capability]) {
|
||||
return Promise.reject(new Error('User not allowed to perform operation'));
|
||||
}
|
||||
|
||||
var method = exists ? 'updateEvent' : 'createEvent';
|
||||
try {
|
||||
yield provider[method](event.data);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeEvent(event) {
|
||||
return co(function *() {
|
||||
var store = core.storeFactory.get('Event');
|
||||
var provider = yield store.providerFor(event);
|
||||
var capabilities;
|
||||
try {
|
||||
yield provider.eventCapabilities(event.data);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
if (!capabilities.canDelete) {
|
||||
return Promise.reject(new Error('User not allowed to perform operation'));
|
||||
}
|
||||
|
||||
yield provider.deleteEvent(event.data);
|
||||
});
|
||||
}
|
||||
|
||||
function setSetting(key, value) {
|
||||
var store = core.storeFactory.get('Setting');
|
||||
return store.set(key, value);
|
||||
}
|
||||
|
||||
function listAccounts(stream) {
|
||||
// TODO
|
||||
console.log('accounts/list', stream);
|
||||
}
|
||||
|
||||
function getAccount(stream, id) {
|
||||
// TODO
|
||||
console.log('accounts/get', stream, id);
|
||||
}
|
||||
|
||||
function listCalendars(stream) {
|
||||
// TODO
|
||||
console.log('calendars/list', stream);
|
||||
}
|
||||
|
||||
function getEvent(stream, id) {
|
||||
// TODO
|
||||
console.log('events/get', stream, id);
|
||||
}
|
||||
|
||||
function listBusytimes(stream, day) {
|
||||
// TODO
|
||||
console.log('busytimes/list', stream, day);
|
||||
}
|
||||
|
||||
function getSetting(stream, key) {
|
||||
// TODO
|
||||
console.log('settings/set', stream, key);
|
||||
}
|
||||
|
||||
method('echo', echo);
|
||||
method('accounts/create', saveAccount);
|
||||
method('accounts/update', saveAccount);
|
||||
method('accounts/remove', removeAccount);
|
||||
method('accounts/sync', syncAccount);
|
||||
method('events/create', saveEvent.bind(null, true));
|
||||
method('events/update', saveEvent.bind(null, true));
|
||||
method('events/remove', removeEvent);
|
||||
method('settings/set', setSetting);
|
||||
stream('accounts/list', listAccounts);
|
||||
stream('accounts/get', getAccount);
|
||||
stream('calendars/list', listCalendars);
|
||||
stream('events/get', getEvent);
|
||||
stream('busytimes/list', listBusytimes);
|
||||
stream('settings/get', getSetting);
|
||||
|
||||
exports.start = start;
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
importScripts('../ext/alameda.js');
|
||||
|
||||
require.config({
|
||||
baseUrl: '/js/backend',
|
||||
paths: {
|
||||
common: '/js/common',
|
||||
ext: '/js/ext'
|
||||
}
|
||||
});
|
||||
|
||||
require(['calendar_service'], service => service.start());
|
||||
14
assets/webapps/calendar.gaiamobile.org/js/backend/core.js
Normal file
14
assets/webapps/calendar.gaiamobile.org/js/backend/core.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
define(['require','exports','module'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
// this module is a "temporary" solution for all the circular dependencies
|
||||
// that we have on the calendar app at the moment. we inject all the
|
||||
// dependencies since stores, providers and db are tight coupled.
|
||||
// the plan is to not rely on the "app" namespace since we are splitting the
|
||||
// code into "backend" (worker) and "frontend" (main thread)
|
||||
|
||||
exports.db = null;
|
||||
exports.providerFactory = null;
|
||||
exports.storeFactory = null;
|
||||
|
||||
});
|
||||
457
assets/webapps/calendar.gaiamobile.org/js/backend/db.js
Normal file
457
assets/webapps/calendar.gaiamobile.org/js/backend/db.js
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
/* jshint loopfunc: true */
|
||||
define(['require','exports','module','models/account','provider/local','common/presets','common/responder','core','common/debug','common/promise','common/constants','common/next_tick','common/probably_parse_int','ext/uuid'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Account = require('models/account');
|
||||
var Local = require('provider/local');
|
||||
var Presets = require('common/presets');
|
||||
var Responder = require('common/responder');
|
||||
var core = require('core');
|
||||
var debug = require('common/debug')('db');
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
var localCalendarId = require('common/constants').localCalendarId;
|
||||
var nextTick = require('common/next_tick');
|
||||
var probablyParseInt = require('common/probably_parse_int');
|
||||
var uuid = require('ext/uuid');
|
||||
|
||||
var idb = self.indexedDB;
|
||||
|
||||
const VERSION = 15;
|
||||
|
||||
var store = Object.freeze({
|
||||
events: 'events',
|
||||
accounts: 'accounts',
|
||||
calendars: 'calendars',
|
||||
busytimes: 'busytimes',
|
||||
settings: 'settings',
|
||||
alarms: 'alarms',
|
||||
icalComponents: 'icalComponents'
|
||||
});
|
||||
|
||||
function Db(name) {
|
||||
this.name = name;
|
||||
Responder.call(this);
|
||||
this._upgradeOperations = [];
|
||||
|
||||
denodeifyAll(this, ['load']);
|
||||
}
|
||||
module.exports = Db;
|
||||
|
||||
Db.prototype = {
|
||||
__proto__: Responder.prototype,
|
||||
|
||||
/**
|
||||
* Database connection
|
||||
*/
|
||||
connection: null,
|
||||
|
||||
load: function(callback) {
|
||||
debug(`Will load ${this.name} db.`);
|
||||
|
||||
var self = this;
|
||||
function setupDefaults() {
|
||||
if (self.oldVersion < 8) {
|
||||
self._setupDefaults(callback);
|
||||
} else {
|
||||
nextTick(callback);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isOpen) {
|
||||
return setupDefaults();
|
||||
}
|
||||
|
||||
this.open(VERSION, setupDefaults);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Opens connection to database.
|
||||
*
|
||||
* @param {Numeric} [version] version of database to open.
|
||||
* default to current version.
|
||||
* Should _only_ be used in testing.
|
||||
*
|
||||
* @param {Function} [callback] first argument is error, second
|
||||
* is result of operation or null
|
||||
* in the error case.
|
||||
*/
|
||||
open: function(version, callback) {
|
||||
if (typeof(version) === 'function') {
|
||||
callback = version;
|
||||
version = VERSION;
|
||||
}
|
||||
|
||||
var req = idb.open(this.name, version);
|
||||
this.version = version;
|
||||
|
||||
var self = this;
|
||||
|
||||
req.onsuccess = function() {
|
||||
self.isOpen = true;
|
||||
self.connection = req.result;
|
||||
|
||||
// if we have pending upgrade operations
|
||||
if (self._upgradeOperations.length) {
|
||||
var pending = self._upgradeOperations.length;
|
||||
|
||||
var operation;
|
||||
while ((operation = self._upgradeOperations.shift())) {
|
||||
operation.call(self, function next() {
|
||||
if (!(--pending)) {
|
||||
callback(null, self);
|
||||
self.emit('open', self);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
callback(null, self);
|
||||
self.emit('open', self);
|
||||
}
|
||||
};
|
||||
|
||||
req.onblocked = function(error) {
|
||||
callback(error, null);
|
||||
self.emit('error', error);
|
||||
};
|
||||
|
||||
req.onupgradeneeded = function(event) {
|
||||
self._handleVersionChange(req.result, event);
|
||||
};
|
||||
|
||||
req.onerror = function(error) {
|
||||
//TODO: steal asuth's error handling...
|
||||
callback(error, null);
|
||||
self.emit('error', error);
|
||||
};
|
||||
},
|
||||
|
||||
transaction: function(list, state) {
|
||||
var names;
|
||||
var self = this;
|
||||
|
||||
if (typeof(list) === 'string') {
|
||||
names = [];
|
||||
names.push(this.store[list] || list);
|
||||
} else {
|
||||
names = list.map(function(name) {
|
||||
return self.store[name] || name;
|
||||
});
|
||||
}
|
||||
|
||||
return this.connection.transaction(names, state || 'readonly');
|
||||
},
|
||||
|
||||
_handleVersionChange: function(db, event) {
|
||||
var newVersion = event.newVersion;
|
||||
var curVersion = event.oldVersion;
|
||||
var transaction = event.currentTarget.transaction;
|
||||
|
||||
this.hasUpgraded = true;
|
||||
this.oldVersion = curVersion;
|
||||
this.upgradedVersion = newVersion;
|
||||
|
||||
for (; curVersion < newVersion; curVersion++) {
|
||||
// if version is < 7 then it was from pre-production
|
||||
// db and we can safely discard its information.
|
||||
if (curVersion < 6) {
|
||||
// ensure clean state if this was an old db.
|
||||
var existingNames = db.objectStoreNames;
|
||||
for (var i = 0; i < existingNames.length; i++) {
|
||||
db.deleteObjectStore(existingNames[i]);
|
||||
}
|
||||
|
||||
// version 0-r are not maintained increment to 6
|
||||
curVersion = 6;
|
||||
|
||||
// busytimes has one event, has one calendar
|
||||
var busytimes = db.createObjectStore(
|
||||
store.busytimes,
|
||||
{ keyPath: '_id' }
|
||||
);
|
||||
|
||||
busytimes.createIndex(
|
||||
'end',
|
||||
'end.utc',
|
||||
{ unique: false, multiEntry: false }
|
||||
);
|
||||
|
||||
busytimes.createIndex(
|
||||
'eventId',
|
||||
'eventId',
|
||||
{ unique: false, multiEntry: false }
|
||||
);
|
||||
|
||||
// events -> belongs to calendar
|
||||
var events = db.createObjectStore(
|
||||
store.events,
|
||||
{ keyPath: '_id' }
|
||||
);
|
||||
|
||||
events.createIndex(
|
||||
'calendarId',
|
||||
'calendarId',
|
||||
{ unique: false, multiEntry: false }
|
||||
);
|
||||
|
||||
events.createIndex(
|
||||
'parentId',
|
||||
'parentId',
|
||||
{ unique: false, multiEntry: false }
|
||||
);
|
||||
|
||||
// accounts -> has many calendars
|
||||
db.createObjectStore(
|
||||
store.accounts, { keyPath: '_id', autoIncrement: true }
|
||||
);
|
||||
|
||||
// calendars -> has many events
|
||||
db.createObjectStore(
|
||||
store.calendars, { keyPath: '_id', autoIncrement: true }
|
||||
);
|
||||
|
||||
} else if (curVersion === 7) {
|
||||
db.createObjectStore(store.settings, { keyPath: '_id' });
|
||||
} else if (curVersion === 8) {
|
||||
var alarms = db.createObjectStore(
|
||||
store.alarms, { keyPath: '_id', autoIncrement: true }
|
||||
);
|
||||
|
||||
alarms.createIndex(
|
||||
'trigger',
|
||||
'trigger.utc',
|
||||
{ unique: false, multiEntry: false }
|
||||
);
|
||||
|
||||
alarms.createIndex(
|
||||
'busytimeId',
|
||||
'busytimeId',
|
||||
{ unique: false, multiEntry: false }
|
||||
);
|
||||
} else if (curVersion === 12) {
|
||||
var icalComponents = db.createObjectStore(
|
||||
store.icalComponents, { keyPath: 'eventId', autoIncrement: false }
|
||||
);
|
||||
|
||||
icalComponents.createIndex(
|
||||
'lastRecurrenceId',
|
||||
'lastRecurrenceId.utc',
|
||||
{ unique: false, multiEntry: false }
|
||||
);
|
||||
} else if (curVersion === 13) {
|
||||
var calendarStore = transaction.objectStore(store.calendars);
|
||||
calendarStore.createIndex(
|
||||
'accountId', 'accountId', { unique: false, multiEntry: false }
|
||||
);
|
||||
} else if (curVersion === 14) {
|
||||
// Bug 851003 - The database may have some busytimes and/or events
|
||||
// which have their calendarId field as a string rather than an int.
|
||||
// We need to fix the calendarIds and also remove any of the idb
|
||||
// objects that have deleted calendars.
|
||||
this.sanitizeEvents(transaction);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 1. Find all events with string calendar ids and index them.
|
||||
* 2. Check for each of the events whether the calendar
|
||||
* they reference still exists.
|
||||
* 3. Fix the events' calendarIds if the calendar still exists else
|
||||
* delete them.
|
||||
* @param {IDBTransaction} trans The active idb transaction during db
|
||||
* upgrade.
|
||||
*/
|
||||
sanitizeEvents: function(trans) {
|
||||
/**
|
||||
* Map from calendar ids to lists of event ids
|
||||
* which we've fixed with that id.
|
||||
* @type {Object.<number, Array.<number>>}
|
||||
*/
|
||||
var badCalendarIdToEventIds = {};
|
||||
|
||||
var objectStore = trans.objectStore(store.events);
|
||||
objectStore.openCursor().onsuccess = (function(evt) {
|
||||
var cursor = evt.target.result;
|
||||
if (!cursor) {
|
||||
return this._updateXorDeleteEvents(badCalendarIdToEventIds, trans);
|
||||
}
|
||||
|
||||
var calendarId = cursor.value.calendarId;
|
||||
if (typeof(calendarId) === 'number') {
|
||||
// Nothing to do here!
|
||||
return cursor.continue();
|
||||
}
|
||||
|
||||
// Record the bad reference.
|
||||
var eventIds = badCalendarIdToEventIds[calendarId] || [];
|
||||
eventIds.push(cursor.key);
|
||||
badCalendarIdToEventIds[calendarId] = eventIds;
|
||||
cursor.continue();
|
||||
}).bind(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* 1. Check for each of the events whether the calendar
|
||||
* they reference still exists.
|
||||
* 2. Fix the events' calendarIds if the calendar still exists else
|
||||
* delete them.
|
||||
*
|
||||
* @param {Object.<number, Array.<number>>} badCalendarIdToEventIds Map
|
||||
* from calendar ids to lists of event ids which we've fixed with
|
||||
* that id.
|
||||
* @param {IDBTransaction} trans The active idb transaction during db
|
||||
* upgrade.
|
||||
* @private
|
||||
*/
|
||||
_updateXorDeleteEvents: function(badCalendarIdToEventIds, trans) {
|
||||
var calendarIds = Object.keys(badCalendarIdToEventIds);
|
||||
calendarIds.forEach(function(calendarId) {
|
||||
//Bug 887698 cases for calendarIds of types strings or integers
|
||||
calendarId = probablyParseInt(calendarId);
|
||||
var eventIds = badCalendarIdToEventIds[calendarId];
|
||||
var calendars = trans.objectStore(store.calendars);
|
||||
calendars.get(calendarId).onsuccess = (function(evt) {
|
||||
var result = evt.target.result;
|
||||
if (result) {
|
||||
this._updateEvents(eventIds, calendarId, trans);
|
||||
} else {
|
||||
this._deleteEvents(eventIds, trans);
|
||||
}
|
||||
}).bind(this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a collection of events and the busytimes that depend on them.
|
||||
*
|
||||
* @param {Array.<number>} eventIds An array of event ids for the events.
|
||||
* @param {number} calendarId A numerical id to set as calendarId.
|
||||
* @param {IDBTransaction} trans The active idb transaction during db
|
||||
* upgrade.
|
||||
* @private
|
||||
*/
|
||||
_updateEvents: function(eventIds, calendarId, trans) {
|
||||
var eventStore = trans.objectStore(store.events);
|
||||
var busytimeStore = trans.objectStore(store.busytimes);
|
||||
var busytimeStoreIndexedByEventId = busytimeStore.index('eventId');
|
||||
|
||||
eventIds.forEach(function(eventId) {
|
||||
eventStore.get(eventId).onsuccess = function(evt) {
|
||||
var result = evt.target.result;
|
||||
result.calendarId = calendarId;
|
||||
eventStore.put(result);
|
||||
};
|
||||
|
||||
busytimeStoreIndexedByEventId.get(eventId).onsuccess = function(evt) {
|
||||
var result = evt.target.result;
|
||||
result.calendarId = calendarId;
|
||||
busytimeStore.put(result);
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a collection of events and the busytimes that depend on them.
|
||||
*
|
||||
* @param {Array.<number>} eventIds An array of event ids for the events.
|
||||
* @param {IDBTransaction} trans The active idb transaction during db
|
||||
* upgrade.
|
||||
* @private
|
||||
*/
|
||||
_deleteEvents: function(eventIds, trans) {
|
||||
var events = core.storeFactory.get('Event');
|
||||
eventIds.forEach(function(eventId) {
|
||||
events.remove(eventId, trans);
|
||||
});
|
||||
},
|
||||
|
||||
get store() {
|
||||
return store;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut method for connection.close
|
||||
*/
|
||||
close: function() {
|
||||
if (this.connection) {
|
||||
this.isOpen = false;
|
||||
this.connection.close();
|
||||
this.connection = null;
|
||||
}
|
||||
},
|
||||
|
||||
clearNonCredentials: function(callback) {
|
||||
var stores = ['events', 'busytimes'];
|
||||
var trans = this.transaction(
|
||||
stores,
|
||||
'readwrite'
|
||||
);
|
||||
|
||||
trans.addEventListener('complete', callback);
|
||||
|
||||
stores.forEach(function(store) {
|
||||
store = trans.objectStore(store);
|
||||
store.clear();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup default values for initial calendar load.
|
||||
*/
|
||||
_setupDefaults: function(callback) {
|
||||
debug('Will setup defaults.');
|
||||
var storeFactory = core.storeFactory;
|
||||
var calendarStore = storeFactory.get('Calendar');
|
||||
var accountStore = storeFactory.get('Account');
|
||||
|
||||
var trans = this.transaction(
|
||||
['accounts', 'calendars'],
|
||||
'readwrite'
|
||||
);
|
||||
|
||||
if (callback) {
|
||||
trans.addEventListener('error', function(err) {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
trans.addEventListener('complete', function() {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
var options = Presets.local.options;
|
||||
debug('Creating local calendar with options:', options);
|
||||
var account = new Account(options);
|
||||
account.preset = 'local';
|
||||
account._id = uuid();
|
||||
|
||||
var calendar = {
|
||||
_id: localCalendarId,
|
||||
accountId: account._id,
|
||||
remote: Local.defaultCalendar()
|
||||
};
|
||||
|
||||
accountStore.persist(account, trans);
|
||||
calendarStore.persist(calendar, trans);
|
||||
},
|
||||
|
||||
deleteDatabase: function(callback) {
|
||||
var req = idb.deleteDatabase(this.name);
|
||||
|
||||
req.onblocked = function() {
|
||||
// improve interface
|
||||
callback(new Error('blocked'));
|
||||
};
|
||||
|
||||
req.onsuccess = function(event) {
|
||||
callback(null, event);
|
||||
};
|
||||
|
||||
req.onerror = function(event) {
|
||||
callback(event, null);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* EventMutations are a simple wrapper for a
|
||||
* set of idb transactions that span multiple
|
||||
* stores and calling out to the time controller
|
||||
* at appropriate times so it can cache the information.
|
||||
*
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
*
|
||||
* // create an event
|
||||
* var mutation = Calendar.EventMutations.create({
|
||||
* // this class does not handle/process events
|
||||
* // only persisting the records. Busytimes will
|
||||
* // automatically be recreated.
|
||||
* event: event
|
||||
* });
|
||||
*
|
||||
* // add an optional component
|
||||
* // mutation.icalComponent = component;
|
||||
*
|
||||
* mutation.commit(function(err) {
|
||||
* if (err) {
|
||||
* // handle it
|
||||
* }
|
||||
*
|
||||
* // success event/busytime/etc.. has been created
|
||||
* });
|
||||
*
|
||||
*
|
||||
* // update an event:
|
||||
* // update shares an identical api but will
|
||||
* // destroy/recreate associated busytimes with event.
|
||||
* var mutation = Calendar.EventMutations.update({
|
||||
* event: event
|
||||
* });
|
||||
*
|
||||
* mutation.commit(function() {
|
||||
*
|
||||
* });
|
||||
*
|
||||
*
|
||||
*/
|
||||
define(['require','exports','module','common/calc','core','ext/uuid'],function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var Calc = require('common/calc');
|
||||
var core = require('core');
|
||||
var uuid = require('ext/uuid');
|
||||
|
||||
/**
|
||||
* Create a single instance busytime for the given event object.
|
||||
*
|
||||
* @param {Object} event to create busytime for.
|
||||
*/
|
||||
function createBusytime(event) {
|
||||
return {
|
||||
_id: event._id + '-' + uuid.v4(),
|
||||
eventId: event._id,
|
||||
calendarId: event.calendarId,
|
||||
start: event.remote.start,
|
||||
end: event.remote.end
|
||||
};
|
||||
}
|
||||
|
||||
function Create(options) {
|
||||
if (options) {
|
||||
for (var key in options) {
|
||||
if (options.hasOwnProperty(key)) {
|
||||
this[key] = options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Create.prototype = {
|
||||
commit: function(callback) {
|
||||
var storeFactory = core.storeFactory;
|
||||
var alarmStore = storeFactory.get('Alarm');
|
||||
var eventStore = storeFactory.get('Event');
|
||||
var busytimeStore = storeFactory.get('Busytime');
|
||||
var componentStore = storeFactory.get('IcalComponent');
|
||||
|
||||
var trans = core.db.transaction(
|
||||
eventStore._dependentStores,
|
||||
'readwrite'
|
||||
);
|
||||
|
||||
trans.oncomplete = function commitComplete() {
|
||||
callback(null);
|
||||
};
|
||||
|
||||
trans.onerror = function commitError(e) {
|
||||
callback(e.target.error);
|
||||
};
|
||||
|
||||
eventStore.persist(this.event, trans);
|
||||
|
||||
if (!this.busytime) {
|
||||
this.busytime = createBusytime(this.event);
|
||||
}
|
||||
|
||||
busytimeStore.persist(this.busytime, trans);
|
||||
|
||||
if (this.icalComponent) {
|
||||
componentStore.persist(this.icalComponent, trans);
|
||||
}
|
||||
|
||||
var alarms = this.event.remote.alarms;
|
||||
if (alarms && alarms.length) {
|
||||
var i = 0;
|
||||
var len = alarms.length;
|
||||
var now = Date.now();
|
||||
|
||||
var alarmTrans = core.db.transaction(
|
||||
['alarms'],
|
||||
'readwrite'
|
||||
);
|
||||
|
||||
for (; i < len; i++) {
|
||||
|
||||
var alarm = {
|
||||
startDate: {
|
||||
offset: this.busytime.start.offset,
|
||||
utc: this.busytime.start.utc + (alarms[i].trigger * 1000)
|
||||
},
|
||||
eventId: this.busytime.eventId,
|
||||
busytimeId: this.busytime._id
|
||||
};
|
||||
|
||||
var alarmDate = Calc.dateFromTransport(this.busytime.end).valueOf();
|
||||
if (alarmDate < now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
alarmStore.persist(alarm, alarmTrans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function Update() {
|
||||
Create.apply(this, arguments);
|
||||
}
|
||||
|
||||
Update.prototype = {
|
||||
commit: function(callback) {
|
||||
var storeFactory = core.storeFactory;
|
||||
var busytimeStore = storeFactory.get('Busytime');
|
||||
|
||||
var self = this;
|
||||
|
||||
// required so UI knows to refresh even in the
|
||||
// case where the start/end times are the same.
|
||||
busytimeStore.removeEvent(this.event._id, function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
Create.prototype.commit.call(self, callback);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.create = function createMutation(option) {
|
||||
return new Create(option);
|
||||
};
|
||||
|
||||
exports.update = function updateMutation(option) {
|
||||
return new Update(option);
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
define(['require','exports','module'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
function Account(options) {
|
||||
var key;
|
||||
|
||||
if (typeof(options) === 'undefined') {
|
||||
options = {};
|
||||
}
|
||||
|
||||
for (key in options) {
|
||||
if (options.hasOwnProperty(key)) {
|
||||
this[key] = options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = Account;
|
||||
|
||||
Account.prototype = {
|
||||
|
||||
/**
|
||||
* Type of provider this
|
||||
* account requires.
|
||||
*/
|
||||
providerType: null,
|
||||
|
||||
/**
|
||||
* ID for this model always set
|
||||
* by the store when hydrating
|
||||
*/
|
||||
id: null,
|
||||
|
||||
/**
|
||||
* Which preset this model came from.
|
||||
*/
|
||||
preset: null,
|
||||
|
||||
/**
|
||||
* Domain for account
|
||||
*/
|
||||
domain: '',
|
||||
|
||||
/**
|
||||
* url/path for account
|
||||
*/
|
||||
entrypoint: '',
|
||||
|
||||
/**
|
||||
* Location where calendars can be found.
|
||||
* May be the same as entrypoint.
|
||||
*/
|
||||
calendarHome: '',
|
||||
|
||||
/**
|
||||
* username for authentication
|
||||
*/
|
||||
user: '',
|
||||
|
||||
/**
|
||||
* password for authentication
|
||||
*/
|
||||
password: '',
|
||||
|
||||
get fullUrl() {
|
||||
return this.domain + this.entrypoint;
|
||||
},
|
||||
|
||||
set fullUrl(value) {
|
||||
var protocolIdx = value.indexOf('://');
|
||||
|
||||
this.domain = value;
|
||||
this.entrypoint = '/';
|
||||
|
||||
if (protocolIdx !== -1) {
|
||||
protocolIdx += 3;
|
||||
// next chunk
|
||||
var domainChunk = value.substr(protocolIdx);
|
||||
var pathIdx = domainChunk.indexOf('/');
|
||||
|
||||
|
||||
if (pathIdx !== -1) {
|
||||
pathIdx = pathIdx + protocolIdx;
|
||||
|
||||
this.entrypoint = value.substr(pathIdx);
|
||||
this.domain = value.substr(0, pathIdx);
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Data only version of this object.
|
||||
* Used for both passing data between
|
||||
* threads (workers) and persisting data
|
||||
* in indexeddb.
|
||||
*/
|
||||
toJSON: function() {
|
||||
var output = {};
|
||||
var fields = [
|
||||
'entrypoint',
|
||||
'calendarHome',
|
||||
'domain',
|
||||
'password',
|
||||
'user',
|
||||
'providerType',
|
||||
'preset',
|
||||
'oauth',
|
||||
'error'
|
||||
];
|
||||
|
||||
fields.forEach(function(key) {
|
||||
output[key] = this[key];
|
||||
}, this);
|
||||
|
||||
if (this._id || this._id === 0) {
|
||||
output._id = this._id;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
define(['require','exports','module'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
function Cal(options) {
|
||||
if (typeof(options) === 'undefined') {
|
||||
options = {};
|
||||
}
|
||||
|
||||
this.remote = {};
|
||||
|
||||
for (var key in options) {
|
||||
if (options.hasOwnProperty(key)) {
|
||||
this[key] = options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
module.exports = Cal;
|
||||
|
||||
Cal.prototype = {
|
||||
|
||||
/**
|
||||
* Local copy of calendars remote state.
|
||||
* Taken from a calendar providers .toJSON method.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
remote: null,
|
||||
|
||||
/**
|
||||
* The date at which this calendar's events
|
||||
* where synchronized.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
firstEventSyncDate: null,
|
||||
|
||||
/**
|
||||
* Last sync token used in previous
|
||||
* event synchronization.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
lastEventSyncToken: '',
|
||||
|
||||
/**
|
||||
* Last date of event synchronization.
|
||||
* This is not going to be used
|
||||
* for any kind of serious operation
|
||||
* right now this is just for the UI.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
lastEventSyncDate: '',
|
||||
|
||||
/**
|
||||
* Indicates if calendar is displayed
|
||||
* locally in the ui.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
localDisplayed: true,
|
||||
|
||||
/**
|
||||
* Id of account this record
|
||||
* is associated with.
|
||||
*/
|
||||
accountId: '',
|
||||
|
||||
/**
|
||||
* Updates remote with data from a calendar provider.
|
||||
*
|
||||
* @param {Calendar.Provider.Calendar.Abstract} provider remote.
|
||||
*/
|
||||
updateRemote: function(provider) {
|
||||
var data = provider;
|
||||
if ('toJSON' in provider) {
|
||||
data = provider.toJSON();
|
||||
}
|
||||
|
||||
this.remote = data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if local and remote state differ
|
||||
* via sync tokens. Returns true when
|
||||
* local sync token and remote do not match.
|
||||
* Does not account for local changes only
|
||||
* when the server state has changed
|
||||
* and we have not yet synchronized.
|
||||
*
|
||||
* @return {Boolean} true when sync needed.
|
||||
*/
|
||||
eventSyncNeeded: function() {
|
||||
var local = this.lastEventSyncToken;
|
||||
var remote = this.remote.syncToken;
|
||||
|
||||
return local != remote;
|
||||
},
|
||||
|
||||
set name(name) {
|
||||
this.remote.name = name;
|
||||
return this.remote.name;
|
||||
},
|
||||
|
||||
set color(color) {
|
||||
this.remote.color = color;
|
||||
return this.remote.color;
|
||||
},
|
||||
|
||||
set description(description) {
|
||||
this.remote.description = description;
|
||||
return this.remote.description;
|
||||
},
|
||||
|
||||
get name() {
|
||||
return this.remote.name;
|
||||
},
|
||||
|
||||
get color() {
|
||||
var color = this.remote.color;
|
||||
if (color) {
|
||||
if (color.substr(0, 1) === '#') {
|
||||
return color.substr(0, 7);
|
||||
}
|
||||
}
|
||||
return this.remote.color;
|
||||
},
|
||||
|
||||
get description() {
|
||||
return this.remote.description;
|
||||
},
|
||||
|
||||
toJSON: function() {
|
||||
var result = {
|
||||
error: this.error,
|
||||
remote: this.remote,
|
||||
accountId: this.accountId,
|
||||
localDisplayed: this.localDisplayed,
|
||||
lastEventSyncDate: this.lastEventSyncDate,
|
||||
lastEventSyncToken: this.lastEventSyncToken,
|
||||
firstEventSyncDate: this.firstEventSyncDate
|
||||
};
|
||||
|
||||
if (this._id || this._id === 0) {
|
||||
result._id = this._id;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
define(['require','exports','module','common/calc','common/probably_parse_int'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Calc = require('common/calc');
|
||||
var probablyParseInt = require('common/probably_parse_int');
|
||||
|
||||
/**
|
||||
* Creates a wrapper around a event instance from the db
|
||||
*/
|
||||
function Event(data) {
|
||||
var isNew = false;
|
||||
|
||||
if (typeof(data) === 'undefined') {
|
||||
isNew = true;
|
||||
data = Object.create(null);
|
||||
data.remote = {};
|
||||
}
|
||||
|
||||
this.data = data;
|
||||
/** shortcut */
|
||||
var remote = this.remote = this.data.remote;
|
||||
|
||||
if ('start' in remote && !('startDate' in remote)) {
|
||||
remote.startDate = Calc.dateFromTransport(
|
||||
remote.start
|
||||
);
|
||||
}
|
||||
|
||||
if ('end' in remote && !('endDate' in remote)) {
|
||||
remote.endDate = Calc.dateFromTransport(
|
||||
remote.end
|
||||
);
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
this.resetToDefaults();
|
||||
}
|
||||
|
||||
var start = this.remote.startDate;
|
||||
var end = this.remote.endDate;
|
||||
|
||||
// the typeof check is to see if we have already
|
||||
// set the value in resetToDefaults (or prior)
|
||||
if (
|
||||
typeof(this._isAllDay) === 'undefined' &&
|
||||
Calc.isOnlyDate(start) &&
|
||||
Calc.isOnlyDate(end)
|
||||
) {
|
||||
// mostly to handle the case before the time
|
||||
// where we actually managed isAllDay as a setter.
|
||||
this.isAllDay = true;
|
||||
} else {
|
||||
// not on prototype intentionally because
|
||||
// we need to either need to resetToDefaults
|
||||
// or check startDate/endDate in the constructor.
|
||||
this.isAllDay = false;
|
||||
}
|
||||
}
|
||||
module.exports = Event;
|
||||
|
||||
Event.prototype = {
|
||||
|
||||
/**
|
||||
* Sets default values of an event.
|
||||
*/
|
||||
resetToDefaults: function() {
|
||||
var now = new Date();
|
||||
|
||||
this.isAllDay = false;
|
||||
|
||||
this.startDate = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate()
|
||||
);
|
||||
|
||||
this.endDate = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate() + 1
|
||||
);
|
||||
},
|
||||
|
||||
get _id() {
|
||||
return this.data._id;
|
||||
},
|
||||
|
||||
_setDate: function(date, field) {
|
||||
if (!(date instanceof Date)) {
|
||||
throw new TypeError('must pass instance of Date');
|
||||
}
|
||||
|
||||
var allDay = this.isAllDay;
|
||||
|
||||
if (allDay) {
|
||||
// clone the existing date
|
||||
date = new Date(date.valueOf());
|
||||
|
||||
// filter out the stuff we don't care about
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
}
|
||||
|
||||
this.remote[field] = Calc.dateToTransport(
|
||||
date,
|
||||
null, // TODO allow setting tzid
|
||||
allDay
|
||||
);
|
||||
|
||||
this.remote[field + 'Date'] = date;
|
||||
},
|
||||
|
||||
/* start date */
|
||||
|
||||
get startDate() {
|
||||
return this.remote.startDate;
|
||||
},
|
||||
|
||||
set startDate(value) {
|
||||
this._setDate(value, 'start');
|
||||
},
|
||||
|
||||
/* end date */
|
||||
|
||||
get endDate() {
|
||||
return this.remote.endDate;
|
||||
},
|
||||
|
||||
set endDate(value) {
|
||||
this._setDate(value, 'end');
|
||||
},
|
||||
|
||||
set isAllDay(value) {
|
||||
this._isAllDay = value;
|
||||
|
||||
// send values through the their setter.
|
||||
if (this.endDate) {
|
||||
this.endDate = this.endDate;
|
||||
}
|
||||
|
||||
if (this.startDate) {
|
||||
this.startDate = this.startDate;
|
||||
}
|
||||
},
|
||||
|
||||
get isAllDay() {
|
||||
return this._isAllDay;
|
||||
},
|
||||
|
||||
/* associated records */
|
||||
|
||||
get calendarId() {
|
||||
return this.data.calendarId;
|
||||
},
|
||||
|
||||
set calendarId(value) {
|
||||
if (value && typeof(value) !== 'number') {
|
||||
value = probablyParseInt(value);
|
||||
}
|
||||
|
||||
this.data.calendarId = value;
|
||||
},
|
||||
|
||||
/* simple setters */
|
||||
|
||||
get syncToken() {
|
||||
return this.remote.syncToken;
|
||||
},
|
||||
|
||||
set syncToken(value) {
|
||||
this.remote.syncToken = value;
|
||||
},
|
||||
|
||||
get title() {
|
||||
return this.remote.title || '';
|
||||
},
|
||||
|
||||
set title(value) {
|
||||
this.remote.title = value;
|
||||
},
|
||||
|
||||
get description() {
|
||||
return this.remote.description || '';
|
||||
},
|
||||
|
||||
set description(value) {
|
||||
this.remote.description = value;
|
||||
},
|
||||
|
||||
get location() {
|
||||
return this.remote.location || '';
|
||||
},
|
||||
|
||||
set location(value) {
|
||||
this.remote.location = value;
|
||||
return this.remote.location;
|
||||
},
|
||||
|
||||
get alarms() {
|
||||
return this.remote.alarms || [];
|
||||
},
|
||||
|
||||
set alarms(value) {
|
||||
this.remote.alarms = value;
|
||||
return this.remote.alarms;
|
||||
},
|
||||
|
||||
/**
|
||||
* If data doesn't have any errors, the event
|
||||
* takes on the attributes of data.
|
||||
*
|
||||
* @param {Object} data, object that contains
|
||||
* at least some attributes of the event object.
|
||||
*
|
||||
* @return {Object} errors if validationErrors returns erros,
|
||||
* true otherwise.
|
||||
*/
|
||||
updateAttributes: function(data) {
|
||||
var errors = this.validationErrors(data);
|
||||
if (errors) {
|
||||
return errors;
|
||||
}
|
||||
for (var field in data) {
|
||||
this[field] = data[field];
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates the contents of the model.
|
||||
*
|
||||
* Output example:
|
||||
*
|
||||
* [
|
||||
* {
|
||||
* name: 'invalidDate',
|
||||
* properties: ['startDate', 'endDate']
|
||||
* }
|
||||
* //...
|
||||
* ]
|
||||
*
|
||||
* @param {Object} data, optional object that contains
|
||||
* at least some attributes of the event object.
|
||||
* @return {Array|False} see above.
|
||||
*/
|
||||
validationErrors: function(data) {
|
||||
var obj = data || this;
|
||||
var end = obj.endDate.valueOf();
|
||||
var start = obj.startDate.valueOf();
|
||||
var errors = [];
|
||||
|
||||
if (start >= end) {
|
||||
errors.push({
|
||||
name: 'start-after-end'
|
||||
});
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
define(['require','exports','module','common/promise','common/next_tick'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var denodeifyAll = require('common/promise').denodeifyAll;
|
||||
var nextTick = require('common/next_tick');
|
||||
|
||||
function Abstract(options) {
|
||||
var key;
|
||||
for (key in options) {
|
||||
if (options.hasOwnProperty(key)) {
|
||||
this[key] = options[key];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
denodeifyAll(this, [
|
||||
'eventCapabilities',
|
||||
'getAccount',
|
||||
'findCalendars',
|
||||
'syncEvents',
|
||||
'ensureRecurrencesExpanded',
|
||||
'createEvent',
|
||||
'updateEvent',
|
||||
'deleteEvent'
|
||||
]);
|
||||
}
|
||||
module.exports = Abstract;
|
||||
|
||||
Abstract.prototype = {
|
||||
// orange (used by local calendar)
|
||||
defaultColor: '#F97C17',
|
||||
|
||||
/**
|
||||
* Does this provider require credentials.
|
||||
*/
|
||||
useCredentials: false,
|
||||
|
||||
/**
|
||||
* Does this provider require a url.
|
||||
*/
|
||||
useUrl: false,
|
||||
|
||||
/**
|
||||
* Can provider sync with remote server?
|
||||
*/
|
||||
canSync: false,
|
||||
|
||||
/**
|
||||
* Can expand recurring events?
|
||||
*/
|
||||
canExpandRecurringEvents: false,
|
||||
|
||||
/**
|
||||
* - domain: (String)
|
||||
* - password: (String)
|
||||
* - user: (String)
|
||||
*
|
||||
* @param {Object} account user credentials.
|
||||
* @param {Function} callback node style (err, result).
|
||||
*/
|
||||
getAccount: function(account, callback) {},
|
||||
|
||||
/**
|
||||
* Attempts to find all calendars
|
||||
* for a given account.
|
||||
*
|
||||
*
|
||||
* account: (same as getAccount)
|
||||
*
|
||||
* @param {Object} account user credentials.
|
||||
* @param {Function} callback node style (err, result).
|
||||
*/
|
||||
findCalendars: function() {},
|
||||
|
||||
/**
|
||||
* Sync remote and local events.
|
||||
*
|
||||
*/
|
||||
syncEvents: function(account, calendar, callback) {},
|
||||
|
||||
/**
|
||||
* Ensures recurring events are expanded up to the given date.
|
||||
*
|
||||
* Its very important to correctly return the second callback arg
|
||||
* in the subclasses callback. When requiredExpansion is returned as true a
|
||||
* second call will likely be made to ensureRecurrencesExpanded to verify
|
||||
* there are no more pending events for the date (controller handles this).
|
||||
*
|
||||
* @param {Date} date to expand recurring events to.
|
||||
* @param {Function} callback [err, requiredExpansion].
|
||||
* first argument is error, second indicates if any expansion was done.
|
||||
*/
|
||||
ensureRecurrencesExpanded: function(date, callback) {},
|
||||
|
||||
/**
|
||||
* Update an event
|
||||
*
|
||||
* @param {Object} event record from event store.
|
||||
*
|
||||
* @param {Object} [busytime] optional busytime instance
|
||||
* when a busytime is passed the edit is treated
|
||||
* as an "exception" and will only edit the one recurrence
|
||||
* related to the busytime. This may result in the creation
|
||||
* of a new "event" related to the busytime.
|
||||
*/
|
||||
updateEvent: function(event, busytime, callback) {},
|
||||
|
||||
/**
|
||||
* Delete event
|
||||
*
|
||||
* @param {Object} event record from the event store.
|
||||
* @param {Object} [busytime] optional busytime instance
|
||||
* when given it will only remove this occurence/exception
|
||||
* of the event rather then the entire sequence of events.
|
||||
*/
|
||||
deleteEvent: function(event, busytime, callback) {},
|
||||
|
||||
/**
|
||||
* Create an event
|
||||
*/
|
||||
createEvent: function(event, callback) {},
|
||||
|
||||
/**
|
||||
* Returns an object with three keys used to
|
||||
* determine the capabilities of a given calendar.
|
||||
*
|
||||
* - canCreate (Boolean)
|
||||
* - canUpdate (Boolean)
|
||||
* - canDelete (Boolean)
|
||||
*
|
||||
* @param {Object} calendar full calendar details.
|
||||
*/
|
||||
calendarCapabilities: function(calendar) {
|
||||
return {
|
||||
canCreateEvent: true,
|
||||
canUpdateEvent: true,
|
||||
canDeleteEvent: true
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the capabilities of a single event.
|
||||
*
|
||||
* @param {Object} event object.
|
||||
* @param {Function} callback [err, caps].
|
||||
*/
|
||||
eventCapabilities: function(event, callback) {
|
||||
var caps = this.calendarCapabilities();
|
||||
|
||||
nextTick(function() {
|
||||
callback(null, {
|
||||
canCreate: caps.canCreateEvent,
|
||||
canUpdate: caps.canUpdateEvent,
|
||||
canDelete: caps.canDeleteEvent
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,712 @@
|
|||
define(['require','exports','module','./abstract','common/error','common/calc','./caldav_pull_events','common/error','common/error','./local','common/error','core','common/is_offline','event_mutations','common/next_tick'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var Authentication = require('common/error').Authentication;
|
||||
var Calc = require('common/calc');
|
||||
var CaldavPullEvents = require('./caldav_pull_events');
|
||||
var CalendarError = require('common/error');
|
||||
var InvalidServer = require('common/error').InvalidServer;
|
||||
var Local = require('./local');
|
||||
var ServerFailure = require('common/error').ServerFailure;
|
||||
var core = require('core');
|
||||
var isOffline = require('common/is_offline');
|
||||
var mutations = require('event_mutations');
|
||||
var nextTick = require('common/next_tick');
|
||||
|
||||
var CALDAV_ERROR_MAP = {
|
||||
'caldav-authentication': Authentication,
|
||||
'caldav-invalid-entrypoint': InvalidServer,
|
||||
'caldav-server-failure': ServerFailure
|
||||
};
|
||||
|
||||
function mapError(error, detail) {
|
||||
console.error('Error with name:', error.name);
|
||||
var calError = CALDAV_ERROR_MAP[error.name];
|
||||
if (!calError) {
|
||||
calError = new CalendarError(error.name, detail);
|
||||
} else {
|
||||
calError = new calError(detail);
|
||||
}
|
||||
|
||||
return calError;
|
||||
}
|
||||
|
||||
function CaldavProvider() {
|
||||
Abstract.apply(this, arguments);
|
||||
|
||||
var storeFactory = core.storeFactory;
|
||||
this.service = core.serviceController;
|
||||
this.accounts = storeFactory.get('Account');
|
||||
this.busytimes = storeFactory.get('Busytime');
|
||||
this.events = storeFactory.get('Event');
|
||||
this.icalComponents = storeFactory.get('IcalComponent');
|
||||
}
|
||||
module.exports = CaldavProvider;
|
||||
|
||||
CaldavProvider.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
role: 'caldav',
|
||||
useUrl: true,
|
||||
useCredentials: true,
|
||||
canSync: true,
|
||||
canExpandRecurringEvents: true,
|
||||
|
||||
// allow us to test the offline behavior
|
||||
isOffline: isOffline,
|
||||
|
||||
/**
|
||||
* Number of dates in the past to sync.
|
||||
* This is usually from the first sync date.
|
||||
*/
|
||||
daysToSyncInPast: 30,
|
||||
|
||||
canCreateEvent: true,
|
||||
canUpdateEvent: true,
|
||||
canDeleteEvent: true,
|
||||
|
||||
hasAccountSettings: true,
|
||||
|
||||
/**
|
||||
* Error handling can be complex- this is the centralized location where
|
||||
* methods can send their error state and some context (an account).
|
||||
*
|
||||
* this._handleServiceError(
|
||||
* err,
|
||||
* { account: account, calendar: calendar }
|
||||
* );
|
||||
*
|
||||
* @param {Object} rawErr from service.
|
||||
* @param {Object} detail for the error.
|
||||
*/
|
||||
_handleServiceError: function(rawErr, detail) {
|
||||
var calendarErr = mapError(rawErr, detail);
|
||||
|
||||
// when we receive a permanent error we should mark the account with an
|
||||
// error.
|
||||
if (
|
||||
calendarErr instanceof Authentication ||
|
||||
calendarErr instanceof InvalidServer
|
||||
) {
|
||||
// there must always be an account
|
||||
if (detail.account) {
|
||||
// but we only mark it with a permanent error if its persisted.
|
||||
if (detail.account._id) {
|
||||
this.accounts.markWithError(detail.account, calendarErr);
|
||||
}
|
||||
} else {
|
||||
console.error('Permanent server error without an account!');
|
||||
}
|
||||
}
|
||||
|
||||
return calendarErr;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the capabilities of a specific calendar.
|
||||
*
|
||||
* The .remote property should contain a .privilegeSet array
|
||||
* with the caldav specific names of privileges.
|
||||
* In the case where .privilegeSet is missing all privileges are granted.
|
||||
*
|
||||
* (see http://tools.ietf.org/html/rfc3744#section-5.4).
|
||||
*
|
||||
* - write-content: (PUT) can edit/add events
|
||||
* - unbind: (DELETE) remove events
|
||||
*
|
||||
*
|
||||
* There are aggregate values (write for example) but
|
||||
* the spec states the specific permissions must also be expanded
|
||||
* so even if they have full write permissions we only check
|
||||
* for write-content.
|
||||
*
|
||||
* @param {Object} calendar object with caldav remote details.
|
||||
* @return {Object} object with three properties
|
||||
* (canUpdate, canDelete, canCreate).
|
||||
*/
|
||||
calendarCapabilities: function(calendar) {
|
||||
var remote = calendar.remote;
|
||||
|
||||
if (!remote.privilegeSet) {
|
||||
return {
|
||||
canUpdateEvent: true,
|
||||
canDeleteEvent: true,
|
||||
canCreateEvent: true
|
||||
};
|
||||
}
|
||||
|
||||
var privilegeSet = remote.privilegeSet;
|
||||
var canWriteConent = privilegeSet.indexOf('write-content') !== -1;
|
||||
|
||||
return {
|
||||
canUpdateEvent: canWriteConent,
|
||||
canCreateEvent: canWriteConent,
|
||||
canDeleteEvent: privilegeSet.indexOf('unbind') !== -1
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the capabilities of a single event.
|
||||
*
|
||||
* @param {Object} event local object.
|
||||
* @param {Function} callback [err, caps].
|
||||
*/
|
||||
eventCapabilities: function(event, callback) {
|
||||
if (event.remote.isRecurring) {
|
||||
// XXX: for now recurring events cannot be edited
|
||||
nextTick(function() {
|
||||
callback(null, {
|
||||
canUpdate: false,
|
||||
canDelete: false,
|
||||
canCreate: false
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
var calendarStore = core.storeFactory.get('Calendar');
|
||||
|
||||
calendarStore.get(event.calendarId, function(err, calendar) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var caps = this.calendarCapabilities(
|
||||
calendar
|
||||
);
|
||||
|
||||
callback(null, {
|
||||
canCreate: caps.canCreateEvent,
|
||||
canUpdate: caps.canUpdateEvent,
|
||||
canDelete: caps.canDeleteEvent
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
getAccount: function(account, callback) {
|
||||
if (this.bailWhenOffline(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.service.request(
|
||||
'caldav',
|
||||
'getAccount',
|
||||
account,
|
||||
function(err, data) {
|
||||
if (err) {
|
||||
return callback(
|
||||
self._handleServiceError(err, { account: account })
|
||||
);
|
||||
}
|
||||
callback(null, data);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hook to format remote data if needed.
|
||||
*/
|
||||
formatRemoteCalendar: function(calendar) {
|
||||
if (!calendar.color) {
|
||||
calendar.color = this.defaultColor;
|
||||
}
|
||||
|
||||
return calendar;
|
||||
},
|
||||
|
||||
findCalendars: function(account, callback) {
|
||||
if (this.bailWhenOffline(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
function formatCalendars(err, data) {
|
||||
if (err) {
|
||||
return callback(self._handleServiceError(err, {
|
||||
account: account
|
||||
}));
|
||||
}
|
||||
|
||||
// format calendars if needed
|
||||
if (data) {
|
||||
for (var key in data) {
|
||||
data[key] = self.formatRemoteCalendar(data[key]);
|
||||
}
|
||||
}
|
||||
|
||||
callback(err, data);
|
||||
}
|
||||
|
||||
this.service.request(
|
||||
'caldav',
|
||||
'findCalendars',
|
||||
account.toJSON(),
|
||||
formatCalendars
|
||||
);
|
||||
},
|
||||
|
||||
_syncEvents: function(account, calendar, cached, callback) {
|
||||
|
||||
var startDate;
|
||||
// calculate the first date we want to sync
|
||||
if (!calendar.firstEventSyncDate) {
|
||||
startDate = Calc.createDay(new Date());
|
||||
|
||||
// will be persisted if sync is successful (clone is required)
|
||||
calendar.firstEventSyncDate = new Date(
|
||||
startDate.valueOf()
|
||||
);
|
||||
} else {
|
||||
startDate = new Date(calendar.firstEventSyncDate.valueOf());
|
||||
}
|
||||
|
||||
// start date - the amount of days is the sync range
|
||||
startDate.setDate(startDate.getDate() - this.daysToSyncInPast);
|
||||
|
||||
var options = {
|
||||
startDate: startDate,
|
||||
cached: cached
|
||||
};
|
||||
|
||||
var stream = this.service.stream(
|
||||
'caldav',
|
||||
'streamEvents',
|
||||
account.toJSON(),
|
||||
calendar.remote,
|
||||
options
|
||||
);
|
||||
|
||||
var pull = new CaldavPullEvents(stream, {
|
||||
account: account,
|
||||
calendar: calendar
|
||||
});
|
||||
|
||||
var calendarStore = core.storeFactory.get('Calendar');
|
||||
var syncStart = new Date();
|
||||
|
||||
var self = this;
|
||||
stream.request(function(err) {
|
||||
if (err) {
|
||||
return callback(
|
||||
self._handleServiceError(err, {
|
||||
account: account,
|
||||
calendar: calendar
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
var trans = pull.commit(function(commitErr) {
|
||||
if (commitErr) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
|
||||
/**
|
||||
* Successfully synchronizing a calendar indicates we can remove this
|
||||
* error.
|
||||
*/
|
||||
calendar.error = undefined;
|
||||
|
||||
calendar.lastEventSyncToken = calendar.remote.syncToken;
|
||||
calendar.lastEventSyncDate = syncStart;
|
||||
|
||||
calendarStore.persist(calendar, trans);
|
||||
|
||||
});
|
||||
|
||||
return pull;
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds list of event urls & sync tokens.
|
||||
*
|
||||
* @param {Calendar.Model.Calendar} calender model instance.
|
||||
* @param {Function} callback node style [err, results].
|
||||
*/
|
||||
_cachedEventsFor: function(calendar, callback) {
|
||||
var store = core.storeFactory.get('Event');
|
||||
|
||||
store.eventsForCalendar(calendar._id, function(err, results) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var list = Object.create(null);
|
||||
|
||||
var i = 0;
|
||||
var len = results.length;
|
||||
var item;
|
||||
|
||||
for (; i < len; i++) {
|
||||
item = results[i];
|
||||
list[item.remote.url] = {
|
||||
syncToken: item.remote.syncToken,
|
||||
id: item._id
|
||||
};
|
||||
}
|
||||
|
||||
callback(null, list);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync remote and local events for a calendar.
|
||||
*/
|
||||
syncEvents: function(account, calendar, callback) {
|
||||
var self = this;
|
||||
|
||||
if (this.bailWhenOffline(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!calendar._id) {
|
||||
throw new Error('calendar must be assigned an _id');
|
||||
}
|
||||
|
||||
// Don't attempt to sync when provider cannot
|
||||
// or we have matching tokens
|
||||
if ((calendar.lastEventSyncToken &&
|
||||
calendar.lastEventSyncToken === calendar.remote.syncToken)) {
|
||||
return nextTick(callback);
|
||||
}
|
||||
|
||||
this._cachedEventsFor(calendar, function(err, results) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
self._syncEvents(
|
||||
account,
|
||||
calendar,
|
||||
results,
|
||||
callback
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* See abstract for contract details...
|
||||
*
|
||||
* Finds all ical components that have not been expanded
|
||||
* beyond the given point and expands / persists them.
|
||||
*
|
||||
* @param {Date} maxDate maximum date to expand to.
|
||||
* @param {Function} callback [err, didExpand].
|
||||
*/
|
||||
ensureRecurrencesExpanded: function(maxDate, callback) {
|
||||
var self = this;
|
||||
this.icalComponents.findRecurrencesBefore(maxDate,
|
||||
function(err, results) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results.length) {
|
||||
callback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// CaldavPullRequest is based on a calendar/account combination
|
||||
// so we must group all of the outstanding components into
|
||||
// their calendars before we can begin expanding them.
|
||||
var groups = Object.create(null);
|
||||
results.forEach(function(comp) {
|
||||
var calendarId = comp.calendarId;
|
||||
if (!(calendarId in groups)) {
|
||||
groups[calendarId] = [];
|
||||
}
|
||||
|
||||
groups[calendarId].push(comp);
|
||||
});
|
||||
|
||||
var pullGroups = [];
|
||||
var pending = 0;
|
||||
var options = {
|
||||
maxDate: Calc.dateToTransport(maxDate)
|
||||
};
|
||||
|
||||
function next(err, pull) {
|
||||
pullGroups.push(pull);
|
||||
if (!(--pending)) {
|
||||
var trans = core.db.transaction(
|
||||
['icalComponents', 'alarms', 'busytimes'],
|
||||
'readwrite'
|
||||
);
|
||||
|
||||
trans.oncomplete = function() {
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
trans.onerror = function(event) {
|
||||
callback(event.result.error.name);
|
||||
};
|
||||
|
||||
pullGroups.forEach(function(pull) {
|
||||
pull.commit(trans);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (var calendarId in groups) {
|
||||
pending++;
|
||||
self._expandComponents(
|
||||
calendarId,
|
||||
groups[calendarId],
|
||||
options,
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
_expandComponents: function(calendarId, comps, options, callback) {
|
||||
var calStore = core.storeFactory.get('Calendar');
|
||||
|
||||
calStore.ownersOf(calendarId, function(err, owners) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var calendar = owners.calendar;
|
||||
var account = owners.account;
|
||||
|
||||
var stream = this.service.stream(
|
||||
'caldav',
|
||||
'expandComponents',
|
||||
comps,
|
||||
options
|
||||
);
|
||||
|
||||
var pull = new CaldavPullEvents(
|
||||
stream,
|
||||
{
|
||||
account: account,
|
||||
calendar: calendar,
|
||||
stores: [
|
||||
'busytimes', 'alarms', 'icalComponents'
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
stream.request(function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
callback(null, pull);
|
||||
});
|
||||
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
createEvent: function(event, busytime, callback) {
|
||||
if (typeof(busytime) === 'function') {
|
||||
callback = busytime;
|
||||
busytime = null;
|
||||
}
|
||||
|
||||
|
||||
if (this.bailWhenOffline(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.ownersOf(event, fetchOwners);
|
||||
|
||||
var self = this;
|
||||
var calendar;
|
||||
var account;
|
||||
function fetchOwners(err, owners) {
|
||||
calendar = owners.calendar;
|
||||
account = owners.account;
|
||||
|
||||
self.service.request(
|
||||
'caldav',
|
||||
'createEvent',
|
||||
account,
|
||||
calendar.remote,
|
||||
event.remote,
|
||||
handleRequest
|
||||
);
|
||||
}
|
||||
|
||||
function handleRequest(err, remote) {
|
||||
if (err) {
|
||||
return callback(self._handleServiceError(err, {
|
||||
account: account,
|
||||
calendar: calendar
|
||||
}));
|
||||
}
|
||||
|
||||
var event = {
|
||||
_id: calendar._id + '-' + remote.id,
|
||||
calendarId: calendar._id
|
||||
};
|
||||
|
||||
var component = {
|
||||
eventId: event._id,
|
||||
ical: remote.icalComponent
|
||||
};
|
||||
|
||||
delete remote.icalComponent;
|
||||
event.remote = remote;
|
||||
|
||||
var create = mutations.create({
|
||||
event: event,
|
||||
icalComponent: component
|
||||
});
|
||||
|
||||
create.commit(function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, create.busytime, create.event);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateEvent: function(event, busytime, callback) {
|
||||
if (typeof(busytime) === 'function') {
|
||||
callback = busytime;
|
||||
busytime = null;
|
||||
}
|
||||
|
||||
if (this.bailWhenOffline(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.ownersOf(event, fetchOwners);
|
||||
|
||||
var self = this;
|
||||
var calendar;
|
||||
var account;
|
||||
|
||||
function fetchOwners(err, owners) {
|
||||
calendar = owners.calendar;
|
||||
account = owners.account;
|
||||
|
||||
self.icalComponents.get(
|
||||
event._id, fetchComponent
|
||||
);
|
||||
}
|
||||
|
||||
function fetchComponent(err, ical) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var details = {
|
||||
event: event.remote,
|
||||
icalComponent: ical.ical
|
||||
};
|
||||
|
||||
self.service.request(
|
||||
'caldav',
|
||||
'updateEvent',
|
||||
account,
|
||||
calendar.remote,
|
||||
details,
|
||||
handleUpdate
|
||||
);
|
||||
}
|
||||
|
||||
function handleUpdate(err, remote) {
|
||||
if (err) {
|
||||
callback(self._handleServiceError(err, {
|
||||
account: account,
|
||||
calendar: calendar
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
var component = {
|
||||
eventId: event._id,
|
||||
ical: remote.icalComponent
|
||||
};
|
||||
|
||||
delete remote.icalComponent;
|
||||
event.remote = remote;
|
||||
|
||||
var update = mutations.update({
|
||||
event: event,
|
||||
icalComponent: component
|
||||
});
|
||||
|
||||
update.commit(function(err) {
|
||||
if (err) {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
callback(null, update.busytime, update.event);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteEvent: function(event, busytime, callback) {
|
||||
if (typeof(busytime) === 'function') {
|
||||
callback = busytime;
|
||||
busytime = null;
|
||||
}
|
||||
|
||||
if (this.bailWhenOffline(callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.ownersOf(event, fetchOwners);
|
||||
|
||||
var calendar;
|
||||
var account;
|
||||
var self = this;
|
||||
function fetchOwners(err, owners) {
|
||||
calendar = owners.calendar;
|
||||
account = owners.account;
|
||||
|
||||
self.service.request(
|
||||
'caldav',
|
||||
'deleteEvent',
|
||||
account,
|
||||
calendar.remote,
|
||||
event.remote,
|
||||
handleRequest
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function handleRequest(err) {
|
||||
if (err) {
|
||||
callback(self._handleServiceError(err, {
|
||||
account: account,
|
||||
calendar: calendar
|
||||
}));
|
||||
return;
|
||||
}
|
||||
Local.prototype.deleteEvent.call(self, event, busytime, callback);
|
||||
}
|
||||
},
|
||||
|
||||
bailWhenOffline: function(callback) {
|
||||
if (!this.offlineMessage && 'mozL10n' in window.navigator) {
|
||||
this.offlineMessage = window.navigator.mozL10n.get('error-offline');
|
||||
}
|
||||
|
||||
var ret = this.isOffline() && callback;
|
||||
if (ret) {
|
||||
var error = new Error();
|
||||
error.name = 'offline';
|
||||
error.message = this.offlineMessage;
|
||||
callback(error);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
define(['require','exports','module','common/calc','core','common/debug','ext/uuid'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Calc = require('common/calc');
|
||||
var core = require('core');
|
||||
var debug = require('common/debug')('pull events');
|
||||
var uuid = require('ext/uuid');
|
||||
|
||||
/**
|
||||
* Event synchronization class for caldav provider.
|
||||
*
|
||||
* Options:
|
||||
* - account: (Calendar.Models.Account) required
|
||||
* - calendar: (Calendar.Models.Calendar) required
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* // instance of a service stream
|
||||
* var stream;
|
||||
*
|
||||
* var pull = new Calendar.Provider.CaldavPullEvents(stream, {
|
||||
* calendar: calendarModel,
|
||||
* account: accountModel,
|
||||
* });
|
||||
*
|
||||
* stream.request(function() {
|
||||
* // stream is complete here the audit of
|
||||
* // events can be made. They are flushed
|
||||
* // to the cache where possible but not actually
|
||||
* // persisted in the database.
|
||||
*
|
||||
* // assuming we are ready commit the changes
|
||||
* pull.commit(function(err) {
|
||||
* // all changes have been committed at this point.
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* @param {Calendar.Responder} stream event emitter usually
|
||||
* a service stream.
|
||||
* @param {Object} options options for instance (see above).
|
||||
*/
|
||||
function PullEvents(stream, options) {
|
||||
if (options.calendar) {
|
||||
this.calendar = options.calendar;
|
||||
} else {
|
||||
throw new Error('.calendar option must be given');
|
||||
}
|
||||
|
||||
if (options.account) {
|
||||
this.account = options.account;
|
||||
} else {
|
||||
throw new Error('.account option must be given');
|
||||
}
|
||||
|
||||
stream.on('event', this);
|
||||
stream.on('component', this);
|
||||
stream.on('occurrence', this);
|
||||
stream.on('missingEvents', this);
|
||||
|
||||
this.icalQueue = [];
|
||||
this.eventQueue = [];
|
||||
this.busytimeQueue = [];
|
||||
this.alarmQueue = [];
|
||||
|
||||
var storeFactory = core.storeFactory;
|
||||
this._busytimeStore = storeFactory.get('Busytime');
|
||||
|
||||
// Catch account events to watch for mid-sync removal
|
||||
this._accountStore = storeFactory.get('Account');
|
||||
this._accountStore.on('remove', this._onRemoveAccount.bind(this));
|
||||
|
||||
this._aborted = false;
|
||||
this._trans = null;
|
||||
}
|
||||
module.exports = PullEvents;
|
||||
|
||||
PullEvents.prototype = {
|
||||
eventQueue: null,
|
||||
busytimeQueue: null,
|
||||
|
||||
/**
|
||||
* Get db id for busytime.
|
||||
*
|
||||
* @param {Object} busytime service sent busytime.
|
||||
*/
|
||||
busytimeIdFromRemote: function(busytime) {
|
||||
var eventId = this.eventIdFromRemote(busytime, !busytime.isException);
|
||||
|
||||
return busytime.start.utc + '-' +
|
||||
busytime.end.utc + '-' +
|
||||
eventId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get db id for event.
|
||||
*
|
||||
* @param {Object} event service sent event or '.remote'
|
||||
* property in db event.
|
||||
*
|
||||
* @param {Boolean} ignoreException when true will ignore
|
||||
* recurrence exceptions.
|
||||
*
|
||||
* @return {String} database object id.
|
||||
*/
|
||||
eventIdFromRemote: function(event, ignoreException) {
|
||||
var id = event.eventId || event.id;
|
||||
|
||||
if (!ignoreException && event.recurrenceId) {
|
||||
id += '-' + event.recurrenceId.utc;
|
||||
}
|
||||
|
||||
return this.calendar._id + '-' + id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format an incoming event.
|
||||
*
|
||||
* @param {Object} event service sent event.
|
||||
*/
|
||||
formatEvent: function(event) {
|
||||
// get id or parent id we ignore the exception
|
||||
// rules here so children (exceptions) can lookup
|
||||
// their parents id.
|
||||
var id = this.eventIdFromRemote(event, true);
|
||||
|
||||
var result = Object.create(null);
|
||||
result.calendarId = this.calendar._id;
|
||||
result.remote = event;
|
||||
|
||||
if (event.recurrenceId) {
|
||||
result.parentId = id;
|
||||
// don't ignore the exceptions
|
||||
result._id = this.eventIdFromRemote(event);
|
||||
} else {
|
||||
result._id = id;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats and tags busytime sent from service.
|
||||
*
|
||||
* @param {Object} time service sent busytime.
|
||||
*/
|
||||
formatBusytime: function(time) {
|
||||
var eventId = this.eventIdFromRemote(time, !time.isException);
|
||||
var id = eventId + '-' + uuid.v4();
|
||||
var calendarId = this.calendar._id;
|
||||
|
||||
time._id = id;
|
||||
time.calendarId = calendarId;
|
||||
time.eventId = eventId;
|
||||
|
||||
if (time.alarms) {
|
||||
var i = 0;
|
||||
var len = time.alarms.length;
|
||||
var alarm;
|
||||
|
||||
for (; i < len; i++) {
|
||||
alarm = time.alarms[i];
|
||||
alarm.eventId = eventId;
|
||||
alarm.busytimeId = id;
|
||||
}
|
||||
}
|
||||
|
||||
return this._busytimeStore.initRecord(time);
|
||||
},
|
||||
|
||||
handleOccurrenceSync: function(item) {
|
||||
var alarms;
|
||||
|
||||
if ('alarms' in item) {
|
||||
alarms = item.alarms;
|
||||
delete item.alarms;
|
||||
|
||||
if (alarms.length) {
|
||||
var i = 0;
|
||||
var len = alarms.length;
|
||||
var now = Date.now();
|
||||
|
||||
for (; i < len; i++) {
|
||||
var alarm = {
|
||||
startDate: {},
|
||||
eventId: item.eventId,
|
||||
busytimeId: item._id
|
||||
};
|
||||
|
||||
// Copy the start object
|
||||
for (var j in item.start) {
|
||||
alarm.startDate[j] = item.start[j];
|
||||
}
|
||||
alarm.startDate.utc += (alarms[i].trigger * 1000);
|
||||
|
||||
var alarmDate = Calc.dateFromTransport(item.end);
|
||||
if (alarmDate.valueOf() < now) {
|
||||
continue;
|
||||
}
|
||||
this.alarmQueue.push(alarm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.busytimeQueue.push(item);
|
||||
},
|
||||
|
||||
handleComponentSync: function(component) {
|
||||
component.eventId = this.eventIdFromRemote(component);
|
||||
component.calendarId = this.calendar._id;
|
||||
|
||||
if (!component.lastRecurrenceId) {
|
||||
delete component.lastRecurrenceId;
|
||||
}
|
||||
|
||||
this.icalQueue.push(component);
|
||||
},
|
||||
|
||||
handleEventSync: function(event) {
|
||||
var exceptions = event.remote.exceptions;
|
||||
delete event.remote.exceptions;
|
||||
|
||||
var id = event._id;
|
||||
|
||||
// clear any busytimes that could possibly be
|
||||
// related to this event as we will be adding new
|
||||
// ones as part of the sync.
|
||||
this._busytimeStore.removeEvent(id);
|
||||
|
||||
this.eventQueue.push(event);
|
||||
|
||||
var component = event.remote.icalComponent;
|
||||
delete event.remote.icalComponent;
|
||||
|
||||
// don't save components for exceptions.
|
||||
// the parent has the ical data.
|
||||
if (!event.remote.recurrenceId) {
|
||||
this.icalQueue.push({
|
||||
data: component,
|
||||
eventId: event._id
|
||||
});
|
||||
}
|
||||
|
||||
if (exceptions) {
|
||||
for (var i = 0; i < exceptions.length; i++) {
|
||||
this.handleEventSync(this.formatEvent(exceptions[i]));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Account removal event handler. Aborts the rest of sync processing, if
|
||||
* the account deleted is the subject of the current sync.
|
||||
*
|
||||
* @param {String} database object id.
|
||||
*/
|
||||
_onRemoveAccount: function(id) {
|
||||
if (id === this.account._id) {
|
||||
// This is our account, so abort the sync.
|
||||
this.abort();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Abort the sync. After this, further events will be ignored and commit()
|
||||
* will do nothing.
|
||||
*/
|
||||
abort: function() {
|
||||
if (this._aborted) {
|
||||
// Bail, if already aborted.
|
||||
return;
|
||||
}
|
||||
// Flag that the sync should be aborted.
|
||||
this._aborted = true;
|
||||
if (this._trans) {
|
||||
// Attempt to abort the in-progress commit transaction
|
||||
this._trans.abort();
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent: function(event) {
|
||||
if (this._aborted) {
|
||||
// Ignore all events, if the sync has been aborted.
|
||||
return;
|
||||
}
|
||||
|
||||
var data = event.data;
|
||||
|
||||
switch (event.type) {
|
||||
case 'missingEvents':
|
||||
this.removeList = data[0];
|
||||
break;
|
||||
case 'occurrence':
|
||||
var occur = this.formatBusytime(data[0]);
|
||||
this.handleOccurrenceSync(occur);
|
||||
break;
|
||||
case 'component':
|
||||
this.handleComponentSync(data[0]);
|
||||
break;
|
||||
case 'event':
|
||||
var e = this.formatEvent(data[0]);
|
||||
this.handleEventSync(e);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Commit all pending records.
|
||||
*
|
||||
*
|
||||
* @param {IDBTransaction} [trans] optional transaction.
|
||||
* @param {Function} callback fired when transaction completes.
|
||||
*/
|
||||
commit: function(trans, callback) {
|
||||
var storeFactory = core.storeFactory;
|
||||
var eventStore = storeFactory.get('Event');
|
||||
var icalComponentStore = storeFactory.get('IcalComponent');
|
||||
var busytimeStore = storeFactory.get('Busytime');
|
||||
var alarmStore = storeFactory.get('Alarm');
|
||||
|
||||
if (typeof(trans) === 'function') {
|
||||
callback = trans;
|
||||
trans = core.db.transaction(
|
||||
['calendars', 'events', 'busytimes', 'alarms', 'icalComponents'],
|
||||
'readwrite'
|
||||
);
|
||||
}
|
||||
|
||||
if (this._aborted) {
|
||||
// Commit nothing, if sync was aborted.
|
||||
return callback && callback(null);
|
||||
}
|
||||
|
||||
// Stash a reference to the transaction, in case we still need to abort.
|
||||
this._trans = trans;
|
||||
|
||||
var self = this;
|
||||
|
||||
this.eventQueue.forEach(function(event) {
|
||||
debug('add event', event);
|
||||
eventStore.persist(event, trans);
|
||||
});
|
||||
|
||||
this.icalQueue.forEach(function(ical) {
|
||||
debug('add component', ical);
|
||||
icalComponentStore.persist(ical, trans);
|
||||
});
|
||||
|
||||
this.busytimeQueue.forEach(function(busy) {
|
||||
debug('add busytime', busy);
|
||||
busytimeStore.persist(busy, trans);
|
||||
});
|
||||
|
||||
this.alarmQueue.forEach(function(alarm) {
|
||||
debug('add alarm', alarm);
|
||||
alarmStore.persist(alarm, trans);
|
||||
});
|
||||
|
||||
if (this.removeList) {
|
||||
this.removeList.forEach(function(id) {
|
||||
eventStore.remove(id, trans);
|
||||
});
|
||||
}
|
||||
|
||||
function handleError(e) {
|
||||
if (e && e.type !== 'abort') {
|
||||
console.error('Error persisting sync results', e);
|
||||
}
|
||||
|
||||
// if we have an event preventDefault so we don't trigger window.onerror
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
self._trans = null;
|
||||
callback && callback(e);
|
||||
}
|
||||
|
||||
trans.addEventListener('error', handleError);
|
||||
trans.addEventListener('abort', handleError);
|
||||
|
||||
|
||||
trans.addEventListener('complete', function() {
|
||||
self._trans = null;
|
||||
callback && callback(null);
|
||||
});
|
||||
|
||||
return trans;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
define(['require','exports','module','common/calc'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Calc = require('common/calc');
|
||||
|
||||
/**
|
||||
* This is a more crude version of what asuth does in email.
|
||||
* Right now this class is here only for debugging sync issues.
|
||||
* We need to add the settings so we can optionally turn this on.
|
||||
*/
|
||||
function EventLogger() {
|
||||
this.events = Object.create(null);
|
||||
this.occurs = Object.create(null);
|
||||
|
||||
this.richLog = {};
|
||||
}
|
||||
module.exports = EventLogger;
|
||||
|
||||
EventLogger.prototype = {
|
||||
displayLog: function(id, string) {
|
||||
if (!(id in this.richLog)) {
|
||||
this.richLog[id] = [];
|
||||
}
|
||||
this.richLog[id].push(string);
|
||||
},
|
||||
|
||||
addEvent: function(event) {
|
||||
var id = event.id;
|
||||
this.events[id] = event;
|
||||
var log = this.formatEvent(event);
|
||||
this.displayLog(id, log);
|
||||
},
|
||||
|
||||
addBusytime: function(busy) {
|
||||
var id = busy.eventId;
|
||||
this.occurs[id] = busy;
|
||||
this.displayLog(id, this.formatBusytime(busy));
|
||||
},
|
||||
|
||||
formatEvent: function(event) {
|
||||
var format = [
|
||||
'add event: (' + event.id + ')',
|
||||
'title:' + event.title,
|
||||
'start:' + (Calc.dateFromTransport(event.start)).toString(),
|
||||
'end:' + (Calc.dateFromTransport(event.end)).toString(),
|
||||
'isException:' + event.isException
|
||||
];
|
||||
|
||||
return format.join(' || ');
|
||||
},
|
||||
|
||||
formatBusytime: function(busy) {
|
||||
var event = this.events[busy.eventId];
|
||||
var title = 'busytime for event: ' + busy.eventId;
|
||||
|
||||
if (event) {
|
||||
title = 'busytime for event: ' + event.title;
|
||||
}
|
||||
|
||||
var format = [
|
||||
title,
|
||||
'start:' + (Calc.dateFromTransport(busy.start)).toString(),
|
||||
'end:' + (Calc.dateFromTransport(busy.end)).toString(),
|
||||
'isException:' + busy.isException
|
||||
];
|
||||
|
||||
return format.join(' || ');
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
define(['require','exports','module','common/debug','./caldav','./local'],function(require, exports) {
|
||||
'use strict';
|
||||
|
||||
var debug = require('common/debug')('provider/factory');
|
||||
|
||||
var ctors = {
|
||||
Caldav: require('./caldav'),
|
||||
Local: require('./local')
|
||||
};
|
||||
|
||||
var providers = exports.providers = Object.create(null);
|
||||
|
||||
exports.get = function(name) {
|
||||
if (!providers[name]) {
|
||||
try {
|
||||
debug(`Initializing provider ${name}`);
|
||||
providers[name] = new ctors[name]();
|
||||
} catch (e) {
|
||||
console.error('Error', e.name, e.message);
|
||||
console.error('Failed to initialize provider', name, e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
return providers[name];
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
define(['require','exports','module','./abstract','core','common/constants','event_mutations','ext/uuid'],function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Abstract = require('./abstract');
|
||||
var core = require('core');
|
||||
var localCalendarId = require('common/constants').localCalendarId;
|
||||
var mutations = require('event_mutations');
|
||||
var uuid = require('ext/uuid');
|
||||
|
||||
function Local() {
|
||||
Abstract.apply(this, arguments);
|
||||
|
||||
var storeFactory = core.storeFactory;
|
||||
this.events = storeFactory.get('Event');
|
||||
this.busytimes = storeFactory.get('Busytime');
|
||||
this.alarms = storeFactory.get('Alarm');
|
||||
}
|
||||
module.exports = Local;
|
||||
|
||||
/**
|
||||
* Returns the details for the default calendars.
|
||||
*/
|
||||
Local.defaultCalendar = function() {
|
||||
// XXX: Make async
|
||||
//var l10nId = 'calendar-local';
|
||||
var name;
|
||||
|
||||
/* TODO(gareth): Make l10n available in backend.
|
||||
if ('mozL10n' in window.navigator) {
|
||||
name = window.navigator.mozL10n.get(l10nId);
|
||||
if (name === l10nId) {
|
||||
name = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
name = 'Offline calendar';
|
||||
}
|
||||
*/
|
||||
name = 'Offline calendar';
|
||||
|
||||
return {
|
||||
// XXX localize this name somewhere
|
||||
name: name,
|
||||
id: localCalendarId,
|
||||
color: Local.prototype.defaultColor
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
Local.prototype = {
|
||||
__proto__: Abstract.prototype,
|
||||
|
||||
canExpandRecurringEvents: false,
|
||||
|
||||
getAccount: function(account, callback) {
|
||||
callback(null, {});
|
||||
},
|
||||
|
||||
findCalendars: function(account, callback) {
|
||||
var list = {};
|
||||
list[localCalendarId] = Local.defaultCalendar();
|
||||
callback(null, list);
|
||||
},
|
||||
|
||||
syncEvents: function(account, calendar, cb) {
|
||||
cb(null);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Calendar.EventMutations.Create} mutation object.
|
||||
*/
|
||||
createEvent: function(event, callback) {
|
||||
// most providers come with their own built in
|
||||
// id system when creating a local event we need to generate
|
||||
// our own UUID.
|
||||
if (!event.remote.id) {
|
||||
// TOOD: uuid is provided by ext/uuid.js
|
||||
// if/when platform supports a safe
|
||||
// random number generator (values never conflict)
|
||||
// we can use that instead of uuid.js
|
||||
event.remote.id = uuid();
|
||||
}
|
||||
|
||||
var create = mutations.create({ event: event });
|
||||
create.commit(function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, create.busytime, create.event);
|
||||
});
|
||||
|
||||
return create;
|
||||
},
|
||||
|
||||
deleteEvent: function(event, busytime, callback) {
|
||||
if (typeof(busytime) === 'function') {
|
||||
callback = busytime;
|
||||
busytime = null;
|
||||
}
|
||||
|
||||
var storeFactory = core.storeFactory;
|
||||
storeFactory.get('Event').remove(event._id, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Calendar.EventMutations.Update} mutation object.
|
||||
*/
|
||||
updateEvent: function(event, busytime, callback) {
|
||||
if (typeof(busytime) === 'function') {
|
||||
callback = busytime;
|
||||
busytime = null;
|
||||
}
|
||||
|
||||
var update = mutations.update({ event: event });
|
||||
update.commit(function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, update.busytime, update.event);
|
||||
});
|
||||
|
||||
return update;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
define(['require','exports','module','common/responder','core','common/promise','common/next_tick'],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;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
define(['require','exports','module','models/account','./abstract','core','common/debug','common/promise','common/extend','common/next_tick','common/probably_parse_int'],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
assets/webapps/calendar.gaiamobile.org/js/backend/store/alarm.js
Normal file
285
assets/webapps/calendar.gaiamobile.org/js/backend/store/alarm.js
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
define(['require','exports','module','./abstract','common/calc','common/create_dom_promise','core','common/debug','common/promise'],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
|
||||
)
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
define(['require','exports','module','./abstract','common/calc','common/binsearch','core','common/compare','common/promise'],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() {}
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
define(['require','exports','module','./abstract','models/calendar','core','common/promise','common/constants','common/probably_parse_int'],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
assets/webapps/calendar.gaiamobile.org/js/backend/store/event.js
Normal file
156
assets/webapps/calendar.gaiamobile.org/js/backend/store/event.js
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
define(['require','exports','module','./abstract','common/calc','./calendar','core','common/promise'],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);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
define(['require','exports','module','common/debug','./account','./alarm','./busytime','./calendar','./event','./ical_component','./setting'],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];
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
define(['require','exports','module','./abstract','common/calc','core','common/promise'],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);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
define(['require','exports','module','./abstract','common/promise','common/next_tick'],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