VulpesOS/gaia/apps/calendar/js/provider/caldav_pull_events.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

391 lines
9.6 KiB
JavaScript
Executable file

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