define(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; } }; });