Jelajahi Sumber

no more EventPeriod

Adam Shaw 8 tahun lalu
induk
melakukan
a03fd7ee1f

+ 4 - 3
src.json

@@ -56,12 +56,13 @@
     "Header.js",
     "Header.js",
     "models/UnzonedRange.js",
     "models/UnzonedRange.js",
     "models/ComponentFootprint.js",
     "models/ComponentFootprint.js",
-    "models/EventManager.js",
+    "models/EventInstanceRepo.js",
     "models/EventInstanceDataSource.js",
     "models/EventInstanceDataSource.js",
     "models/EventInstanceDataSourceSplitter.js",
     "models/EventInstanceDataSourceSplitter.js",
-    "models/EventPeriod.js",
-    "models/EventInstanceRepo.js",
     "models/EventInstanceChangeset.js",
     "models/EventInstanceChangeset.js",
+    "models/EventDataSource.js",
+    "models/RequestableEventDataSource.js",
+    "models/EventManager.js",
     "models/BusinessHourGenerator.js",
     "models/BusinessHourGenerator.js",
     "models/event/EventDefParser.js",
     "models/event/EventDefParser.js",
     "models/event/EventDef.js",
     "models/event/EventDef.js",

+ 5 - 10
src/Calendar.constraints.js

@@ -49,12 +49,12 @@ Calendar.prototype.isEventInstanceGroupAllowed = function(eventInstanceGroup) {
 
 
 
 
 Calendar.prototype.getPeerEventInstances = function(eventDef) {
 Calendar.prototype.getPeerEventInstances = function(eventDef) {
-	return this.eventManager.getEventInstancesWithoutId(eventDef.id);
+	return this.eventManager.instanceRepo.getEventInstancesWithoutId(eventDef.id);
 };
 };
 
 
 
 
 Calendar.prototype.isSelectionFootprintAllowed = function(componentFootprint) {
 Calendar.prototype.isSelectionFootprintAllowed = function(componentFootprint) {
-	var peerEventInstances = this.eventManager.getEventInstances();
+	var peerEventInstances = this.eventManager.instanceRepo.getEventInstances();
 	var peerEventRanges = peerEventInstances.map(eventInstanceToEventRange);
 	var peerEventRanges = peerEventInstances.map(eventInstanceToEventRange);
 	var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges);
 	var peerEventFootprints = this.eventRangesToEventFootprints(peerEventRanges);
 
 
@@ -157,7 +157,7 @@ Calendar.prototype.constraintValToFootprints = function(constraintVal, isAllDay)
 		}
 		}
 	}
 	}
 	else if (constraintVal != null) { // an ID
 	else if (constraintVal != null) { // an ID
-		eventInstances = this.eventManager.getEventInstancesWithId(constraintVal);
+		eventInstances = this.eventManager.instanceRepo.getEventInstancesWithId(constraintVal);
 
 
 		return this.eventInstancesToFootprints(eventInstances);
 		return this.eventInstancesToFootprints(eventInstances);
 	}
 	}
@@ -278,19 +278,14 @@ function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInst
 Returns false on invalid input.
 Returns false on invalid input.
 */
 */
 Calendar.prototype.parseEventDefToInstances = function(eventInput) {
 Calendar.prototype.parseEventDefToInstances = function(eventInput) {
-	var eventPeriod = this.eventManager.currentPeriod;
+	var eventManager = this.eventManager;
 	var eventDef = EventDefParser.parse(eventInput, new EventSource(this));
 	var eventDef = EventDefParser.parse(eventInput, new EventSource(this));
 
 
 	if (!eventDef) { // invalid
 	if (!eventDef) { // invalid
 		return false;
 		return false;
 	}
 	}
 
 
-	if (eventPeriod) {
-		return eventDef.buildInstances(eventPeriod.unzonedRange);
-	}
-	else {
-		return [];
-	}
+	return eventDef.buildInstances(eventManager.currentUnzonedRange);
 };
 };
 
 
 
 

+ 4 - 4
src/Calendar.events-api.js

@@ -120,10 +120,10 @@ Calendar.mixin({
 		var i;
 		var i;
 
 
 		if (legacyQuery == null) { // shortcut for removing all
 		if (legacyQuery == null) { // shortcut for removing all
-			eventManager.removeAllEventDefs();
+			eventManager.removeAllEventDefs(true); // persist=true
 		}
 		}
 		else {
 		else {
-			eventManager.iterEventInstances(function(eventInstance) {
+			eventManager.instanceRepo.iterEventInstances(function(eventInstance) {
 				legacyInstances.push(eventInstance.toLegacy());
 				legacyInstances.push(eventInstance.toLegacy());
 			});
 			});
 
 
@@ -138,7 +138,7 @@ Calendar.mixin({
 			eventManager.freeze();
 			eventManager.freeze();
 
 
 			for (i in idMap) { // reuse `i` as an "id"
 			for (i in idMap) { // reuse `i` as an "id"
-				eventManager.removeEventDefsById(i);
+				eventManager.removeEventDefsById(i, true); // persist=true
 			}
 			}
 
 
 			eventManager.thaw();
 			eventManager.thaw();
@@ -150,7 +150,7 @@ Calendar.mixin({
 	clientEvents: function(legacyQuery) {
 	clientEvents: function(legacyQuery) {
 		var legacyEventInstances = [];
 		var legacyEventInstances = [];
 
 
-		this.eventManager.iterEventInstances(function(eventInstance) {
+		this.eventManager.instanceRepo.iterEventInstances(function(eventInstance) {
 			legacyEventInstances.push(eventInstance.toLegacy());
 			legacyEventInstances.push(eventInstance.toLegacy());
 		});
 		});
 
 

+ 4 - 6
src/Calendar.js

@@ -309,8 +309,6 @@ var Calendar = FC.Calendar = Class.extend(EmitterMixin, ListenerMixin, {
 			rawSources.unshift(singleRawSource);
 			rawSources.unshift(singleRawSource);
 		}
 		}
 
 
-		eventManager.freeze();
-
 		rawSources.forEach(function(rawSource) {
 		rawSources.forEach(function(rawSource) {
 			var source = EventSourceParser.parse(rawSource, _this);
 			var source = EventSourceParser.parse(rawSource, _this);
 
 
@@ -318,19 +316,19 @@ var Calendar = FC.Calendar = Class.extend(EmitterMixin, ListenerMixin, {
 				eventManager.addSource(source);
 				eventManager.addSource(source);
 			}
 			}
 		});
 		});
-
-		eventManager.thaw();
 	},
 	},
 
 
 
 
 	// returns an EventInstanceDataSource
 	// returns an EventInstanceDataSource
 	requestEvents: function(start, end) {
 	requestEvents: function(start, end) {
-		return this.eventManager.requestEvents(
+		this.eventManager.request(
 			start,
 			start,
 			end,
 			end,
 			this.opt('timezone'),
 			this.opt('timezone'),
-			this.opt('lazyFetching')
+			!this.opt('lazyFetching')
 		);
 		);
+
+		return this.eventManager;
 	}
 	}
 
 
 });
 });

+ 2 - 2
src/Calendar.render.js

@@ -144,14 +144,14 @@ Calendar.mixin({
 
 
 
 
 	startBatchRender: function() {
 	startBatchRender: function() {
-		if (!(this.batchRenderDepth++)) {
+		if (!(this.batchRenderDepth++) && this.renderQueue) {
 			this.renderQueue.pause();
 			this.renderQueue.pause();
 		}
 		}
 	},
 	},
 
 
 
 
 	stopBatchRender: function() {
 	stopBatchRender: function() {
-		if (!(--this.batchRenderDepth)) {
+		if (!(--this.batchRenderDepth) && this.renderQueue) {
 			this.renderQueue.resume();
 			this.renderQueue.resume();
 		}
 		}
 	},
 	},

+ 143 - 0
src/models/EventDataSource.js

@@ -0,0 +1,143 @@
+
+/*
+Stores EventDefs AND EventInstances
+*/
+var EventDataSource = EventInstanceDataSource.extend({
+
+	currentUnzonedRange: null, // for creating EventInstances
+
+	eventDefsByUid: null,
+	eventDefsById: null,
+
+
+	constructor: function() {
+		EventInstanceDataSource.call(this);
+
+		this.eventDefsByUid = {};
+		this.eventDefsById = {};
+	},
+
+
+	getEventDefByUid: function(eventDefUid) {
+		return this.eventDefsByUid[eventDefUid];
+	},
+
+
+	getEventDefsById: function(eventDefId) {
+		var bucket = this.eventDefsById[eventDefId];
+
+		if (bucket) {
+			return bucket.slice(); // clone
+		}
+
+		return [];
+	},
+
+
+	addEventDefs: function(eventDefs) {
+		for (var i = 0; i < eventDefs.length; i++) {
+			this.addEventDef(eventDefs[i]);
+		}
+	},
+
+
+	// generates and stores instances as well
+	addEventDef: function(eventDef) {
+		this.storeEventDef(eventDef);
+
+		this.addChangeset(
+			new EventInstanceChangeset(
+				null, // removals
+				new EventInstanceRepo( // additions
+					eventDef.buildInstances(this.currentUnzonedRange)
+				)
+			)
+		);
+	},
+
+
+	// does NOT add any instances
+	storeEventDef: function(eventDef) {
+		var eventDefsById = this.eventDefsById;
+		var id = eventDef.id;
+
+		(eventDefsById[id] || (eventDefsById[id] = []))
+			.push(eventDef);
+
+		this.eventDefsByUid[eventDef.uid] = eventDef;
+	},
+
+
+	removeEventDefsById: function(eventDefId) {
+		var _this = this;
+
+		this.getEventDefsById(eventDefId).forEach(function(eventDef) {
+			_this.removeEventDef(eventDef);
+		});
+	},
+
+
+	removeAllEventDefs: function() {
+		this.freeze();
+
+		Object.values(this.eventDefsByUid).forEach(
+			this.removeEventDef.bind(this)
+		);
+
+		this.thaw();
+	},
+
+
+	removeEventDef: function(eventDef) {
+		var eventDefsById = this.eventDefsById;
+		var bucket = eventDefsById[eventDef.id];
+
+		delete this.eventDefsByUid[eventDef.uid];
+
+		if (bucket) {
+			removeExact(bucket, eventDef);
+
+			if (!bucket.length) {
+				delete eventDefsById[eventDef.id];
+			}
+
+			this.addChangeset(
+				new EventInstanceChangeset(
+					new EventInstanceRepo( // removals
+						this.instanceRepo.getEventInstancesForDef(eventDef)
+					)
+				)
+			);
+		}
+	},
+
+
+	/*
+	Will emit TWO SEPARATE CHANGESETS. This is due to EventDef's being mutable.
+	Returns an undo function.
+	*/
+	mutateEventsWithId: function(eventDefId, eventDefMutation) {
+		var _this = this;
+		var eventDefs = this.getEventDefsById(eventDefId);
+		var undoFuncs;
+
+		eventDefs.forEach(this.removeEventDef.bind(this));
+
+		undoFuncs = eventDefs.map(function(eventDef) {
+			return eventDefMutation.mutateSingle(eventDef);
+		});
+
+		eventDefs.forEach(this.addEventDef.bind(this));
+
+		return function() {
+			eventDefs.forEach(_this.removeEventDef.bind(_this));
+
+			undoFuncs.forEach(function(undoFunc) {
+				undoFunc();
+			});
+
+			eventDefs.forEach(_this.addEventDef.bind(_this));
+		};
+	}
+
+});

+ 37 - 193
src/models/EventManager.js

@@ -1,7 +1,5 @@
 
 
-var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
-
-	currentPeriod: null,
+var EventManager = RequestableEventDataSource.extend({
 
 
 	calendar: null,
 	calendar: null,
 	stickySource: null,
 	stickySource: null,
@@ -9,32 +7,18 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 
 
 
 
 	constructor: function(calendar) {
 	constructor: function(calendar) {
+		RequestableEventDataSource.call(this);
+
 		this.calendar = calendar;
 		this.calendar = calendar;
 		this.stickySource = new ArrayEventSource(calendar);
 		this.stickySource = new ArrayEventSource(calendar);
 		this.otherSources = [];
 		this.otherSources = [];
-	},
-
-
-	// returns an EventInstanceDataSource
-	requestEvents: function(start, end, timezone, force) {
-		if (
-			force ||
-			!this.currentPeriod ||
-			!this.currentPeriod.isWithinRange(start, end)
-		) {
-			this.setPeriod( // will change this.currentPeriod
-				new EventPeriod(start, end, timezone)
-			);
-		}
-
-		return this.currentPeriod;
-	},
 
 
-
-	tryReset: function() {
-		if (this.currentPeriod) {
-			this.currentPeriod.tryReset();
-		}
+		this.on('before:receive', function() {
+			calendar.startBatchRender();
+		});
+		this.on('after:receive', function() {
+			calendar.stopBatchRender();
+		});
 	},
 	},
 
 
 
 
@@ -45,55 +29,21 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 	addSource: function(eventSource) {
 	addSource: function(eventSource) {
 		this.otherSources.push(eventSource);
 		this.otherSources.push(eventSource);
 
 
-		if (this.currentPeriod) {
-			this.currentPeriod.requestSource(eventSource); // might release
+		if (this.currentUnzonedRange) {
+			this.requestSource(eventSource);
 		}
 		}
 	},
 	},
 
 
 
 
 	removeSource: function(doomedSource) {
 	removeSource: function(doomedSource) {
 		removeExact(this.otherSources, doomedSource);
 		removeExact(this.otherSources, doomedSource);
-
-		if (this.currentPeriod) {
-			this.currentPeriod.purgeSource(doomedSource); // might release
-		}
+		this.purgeSource(doomedSource);
 	},
 	},
 
 
 
 
 	removeAllSources: function() {
 	removeAllSources: function() {
 		this.otherSources = [];
 		this.otherSources = [];
-
-		if (this.currentPeriod) {
-			this.currentPeriod.purgeAllSources(); // might release
-		}
-	},
-
-
-	// Source Refetching
-	// -----------------------------------------------------------------------------------------------------------------
-
-
-	refetchSource: function(eventSource) {
-		var currentPeriod = this.currentPeriod;
-
-		if (currentPeriod) {
-			currentPeriod.freeze();
-			currentPeriod.purgeSource(eventSource);
-			currentPeriod.requestSource(eventSource);
-			currentPeriod.thaw();
-		}
-	},
-
-
-	refetchAllSources: function() {
-		var currentPeriod = this.currentPeriod;
-
-		if (currentPeriod) {
-			currentPeriod.freeze();
-			currentPeriod.purgeAllSources();
-			currentPeriod.requestSources(this.getSources());
-			currentPeriod.thaw();
-		}
+		this.purgeAllSources();
 	},
 	},
 
 
 
 
@@ -174,120 +124,38 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 	},
 	},
 
 
 
 
-	// Event-Period
-	// -----------------------------------------------------------------------------------------------------------------
-
-
-	setPeriod: function(eventPeriod) {
-		if (this.currentPeriod) {
-			this.unbindPeriod(this.currentPeriod);
-			this.currentPeriod = null;
-		}
-
-		this.currentPeriod = eventPeriod;
-		this.bindPeriod(eventPeriod);
-
-		eventPeriod.requestSources(this.getSources());
-	},
-
-
-	bindPeriod: function(eventPeriod) {
-		this.listenTo(eventPeriod, {
-			'before:receive': function() {
-				this.calendar.startBatchRender();
-			},
-			'after:receive': function() {
-				this.calendar.stopBatchRender();
-			}
-		});
-	},
-
-
-	unbindPeriod: function(eventPeriod) {
-		this.stopListeningTo(eventPeriod);
-	},
-
-
-	// Event Getting/Adding/Removing
+	// Event Adding/Removing needs to have side-effects in the sources
 	// -----------------------------------------------------------------------------------------------------------------
 	// -----------------------------------------------------------------------------------------------------------------
 
 
 
 
-	getEventDefByUid: function(uid) {
-		if (this.currentPeriod) {
-			return this.currentPeriod.getEventDefByUid(uid);
-		}
-	},
-
-
-	getEventDefsById: function(eventDefId) {
-		if (this.currentPeriod) {
-			return this.currentPeriod.getEventDefsById(eventDefId);
-		}
-		return [];
-	},
-
-
-	iterEventInstances: function(func) {
-		if (this.currentPeriod) {
-			this.currentPeriod.instanceRepo.iterEventInstances(func);
+	addEventDef: function(eventDef, persist) {
+		if (persist) {
+			this.stickySource.addEventDef(eventDef);
 		}
 		}
-	},
 
 
-
-	getEventInstances: function() {
-		if (this.currentPeriod) {
-			return this.currentPeriod.instanceRepo.getEventInstances();
-		}
-		return [];
+		RequestableEventDataSource.prototype.addEventDef.apply(this, arguments);
 	},
 	},
 
 
 
 
-	getEventInstancesWithId: function(eventDefId) {
-		if (this.currentPeriod) {
-			return this.currentPeriod.instanceRepo.getEventInstancesWithId(eventDefId);
+	removeEventDefsById: function(eventId, persist) {
+		if (persist) {
+			this.getSources().forEach(function(eventSource) {
+				eventSource.removeEventDefsById(eventId);
+			});
 		}
 		}
-		return [];
-	},
-
 
 
-	getEventInstancesWithoutId: function(eventDefId) {
-		if (this.currentPeriod) {
-			return this.currentPeriod.instanceRepo.getEventInstancesWithoutId(eventDefId);
-		}
-		return [];
+		RequestableEventDataSource.prototype.removeEventDefsById.apply(this, arguments);
 	},
 	},
 
 
 
 
-	addEventDef: function(eventDef, isSticky) {
-		if (isSticky) {
-			this.stickySource.addEventDef(eventDef);
-		}
-
-		if (this.currentPeriod) {
-			this.currentPeriod.addEventDef(eventDef); // might release
-		}
-	},
-
-
-	removeEventDefsById: function(eventId) {
-		this.getSources().forEach(function(eventSource) {
-			eventSource.removeEventDefsById(eventId);
-		});
-
-		if (this.currentPeriod) {
-			this.currentPeriod.removeEventDefsById(eventId); // might release
+	removeAllEventDefs: function(persist) {
+		if (persist) {
+			this.getSources().forEach(function(eventSource) {
+				eventSource.removeAllEventDefs();
+			});
 		}
 		}
-	},
-
 
 
-	removeAllEventDefs: function() {
-		this.getSources().forEach(function(eventSource) {
-			eventSource.removeAllEventDefs();
-		});
-
-		if (this.currentPeriod) {
-			this.currentPeriod.removeAllEventDefs();
-		}
+		RequestableEventDataSource.prototype.removeAllEventDefs.apply(this, arguments);
 	},
 	},
 
 
 
 
@@ -300,24 +168,18 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 	*/
 	*/
 	mutateEventsWithId: function(eventDefId, eventDefMutation) {
 	mutateEventsWithId: function(eventDefId, eventDefMutation) {
 		var calendar = this.calendar;
 		var calendar = this.calendar;
-		var currentPeriod = this.currentPeriod;
 		var undoFunc;
 		var undoFunc;
 
 
-		if (currentPeriod) {
+		// emits two separate changesets, so make sure rendering happens only once
+		calendar.startBatchRender();
+		undoFunc = RequestableEventDataSource.prototype.mutateEventsWithId.apply(this, arguments);
+		calendar.stopBatchRender();
 
 
-			// emits two separate changesets, so make sure rendering happens only once
+		return function() {
 			calendar.startBatchRender();
 			calendar.startBatchRender();
-			undoFunc = currentPeriod.mutateEventsWithId(eventDefId, eventDefMutation);
+			undoFunc();
 			calendar.stopBatchRender();
 			calendar.stopBatchRender();
-
-			return function() {
-				calendar.startBatchRender();
-				undoFunc();
-				calendar.stopBatchRender();
-			};
-		}
-
-		return function() { };
+		};
 	},
 	},
 
 
 
 
@@ -343,24 +205,6 @@ var EventManager = Class.extend(EmitterMixin, ListenerMixin, {
 		}
 		}
 
 
 		return new EventInstanceGroup(allInstances);
 		return new EventInstanceGroup(allInstances);
-	},
-
-
-	// Freezing
-	// -----------------------------------------------------------------------------------------------------------------
-
-
-	freeze: function() {
-		if (this.currentPeriod) {
-			this.currentPeriod.freeze();
-		}
-	},
-
-
-	thaw: function() {
-		if (this.currentPeriod) {
-			this.currentPeriod.thaw();
-		}
 	}
 	}
 
 
 });
 });

+ 0 - 265
src/models/EventPeriod.js

@@ -1,265 +0,0 @@
-
-var EventPeriod = EventInstanceDataSource.extend({
-
-	start: null,
-	end: null,
-	timezone: null,
-	unzonedRange: null,
-	requestsByUid: null,
-	pendingSourceCnt: 0,
-	eventDefsByUid: null,
-	eventDefsById: null,
-
-
-	constructor: function(start, end, timezone) {
-		EventInstanceDataSource.call(this);
-
-		this.start = start;
-		this.end = end;
-		this.timezone = timezone;
-		this.unzonedRange = new UnzonedRange(
-			start.clone().stripZone(),
-			end.clone().stripZone()
-		);
-		this.requestsByUid = {};
-		this.eventDefsByUid = {};
-		this.eventDefsById = {};
-	},
-
-
-	isWithinRange: function(start, end) {
-		// TODO: use a range util function?
-		return !start.isBefore(this.start) && !end.isAfter(this.end);
-	},
-
-
-	// Sources
-	// -----------------------------------------------------------------------------------------------------------------
-
-
-	requestSources: function(sources) {
-		this.freeze();
-
-		for (var i = 0; i < sources.length; i++) {
-			this.requestSource(sources[i]);
-		}
-
-		this.thaw();
-	},
-
-
-	requestSource: function(source) {
-		var _this = this;
-		var request = { source: source, status: 'pending' };
-
-		this.requestsByUid[source.uid] = request;
-		this.pendingSourceCnt += 1;
-
-		source.fetch(this.start, this.end, this.timezone).then(function(eventDefs) {
-			if (request.status !== 'cancelled') {
-				request.status = 'completed';
-				request.eventDefs = eventDefs;
-
-				_this.addEventDefs(eventDefs);
-				_this.reportSourceDone();
-			}
-		}, function() { // failure
-			if (request.status !== 'cancelled') {
-				request.status = 'failed';
-
-				_this.reportSourceDone();
-			}
-		});
-	},
-
-
-	purgeSource: function(source) {
-		var request = this.requestsByUid[source.uid];
-
-		if (request) {
-			delete this.requestsByUid[source.uid];
-
-			if (request.status === 'pending') {
-				request.status = 'cancelled';
-
-				this.reportSourceDone();
-			}
-			else if (request.status === 'completed') {
-				this.freeze();
-
-				request.eventDefs.forEach(this.removeEventDef.bind(this));
-
-				this.thaw();
-			}
-		}
-	},
-
-
-	purgeAllSources: function() {
-		var requestsByUid = this.requestsByUid;
-		var uid, request;
-		var completedCnt = 0;
-
-		for (uid in requestsByUid) {
-			request = requestsByUid[uid];
-
-			if (request.status === 'pending') {
-				request.status = 'cancelled';
-			}
-			else if (request.status === 'completed') {
-				completedCnt++;
-			}
-		}
-
-		this.pendingSourceCnt = 0;
-		this.requestsByUid = {};
-
-		if (completedCnt) {
-			this.removeAllEventDefs();
-		}
-	},
-
-
-	// Event Definitions
-	// -----------------------------------------------------------------------------------------------------------------
-
-
-	getEventDefByUid: function(eventDefUid) {
-		return this.eventDefsByUid[eventDefUid];
-	},
-
-
-	getEventDefsById: function(eventDefId) {
-		var bucket = this.eventDefsById[eventDefId];
-
-		if (bucket) {
-			return bucket.slice(); // clone
-		}
-
-		return [];
-	},
-
-
-	addEventDefs: function(eventDefs) {
-		for (var i = 0; i < eventDefs.length; i++) {
-			this.addEventDef(eventDefs[i]);
-		}
-	},
-
-
-	// generates and stores instances as well
-	addEventDef: function(eventDef) {
-		this.storeEventDef(eventDef);
-
-		this.addChangeset(
-			new EventInstanceChangeset(
-				null, // removals
-				new EventInstanceRepo( // additions
-					eventDef.buildInstances(this.unzonedRange)
-				)
-			)
-		);
-	},
-
-
-	// does NOT add any instances
-	storeEventDef: function(eventDef) {
-		var eventDefsById = this.eventDefsById;
-		var id = eventDef.id;
-
-		(eventDefsById[id] || (eventDefsById[id] = []))
-			.push(eventDef);
-
-		this.eventDefsByUid[eventDef.uid] = eventDef;
-	},
-
-
-	removeEventDefsById: function(eventDefId) {
-		var _this = this;
-
-		this.getEventDefsById(eventDefId).forEach(function(eventDef) {
-			_this.removeEventDef(eventDef);
-		});
-	},
-
-
-	removeAllEventDefs: function() {
-		this.freeze();
-
-		Object.values(this.eventDefsByUid).forEach(
-			this.removeEventDef.bind(this)
-		);
-
-		this.thaw();
-	},
-
-
-	removeEventDef: function(eventDef) {
-		var eventDefsById = this.eventDefsById;
-		var bucket = eventDefsById[eventDef.id];
-
-		delete this.eventDefsByUid[eventDef.uid];
-
-		if (bucket) {
-			removeExact(bucket, eventDef);
-
-			if (!bucket.length) {
-				delete eventDefsById[eventDef.id];
-			}
-
-			this.addChangeset(
-				new EventInstanceChangeset(
-					new EventInstanceRepo( // removals
-						this.instanceRepo.getEventInstancesForDef(eventDef)
-					)
-				)
-			);
-		}
-	},
-
-
-	/*
-	Will emit TWO SEPARATE CHANGESETS. This is due to EventDef's being mutable.
-	Returns an undo function.
-	*/
-	mutateEventsWithId: function(eventDefId, eventDefMutation) {
-		var _this = this;
-		var eventDefs = this.getEventDefsById(eventDefId);
-		var undoFuncs;
-
-		eventDefs.forEach(this.removeEventDef.bind(this));
-
-		undoFuncs = eventDefs.map(function(eventDef) {
-			return eventDefMutation.mutateSingle(eventDef);
-		});
-
-		eventDefs.forEach(this.addEventDef.bind(this));
-
-		return function() {
-			eventDefs.forEach(_this.removeEventDef.bind(_this));
-
-			undoFuncs.forEach(function(undoFunc) {
-				undoFunc();
-			});
-
-			eventDefs.forEach(_this.addEventDef.bind(_this));
-		};
-	},
-
-
-	// Reporting and Triggering
-	// -----------------------------------------------------------------------------------------------------------------
-
-
-	reportSourceDone: function() {
-		this.pendingSourceCnt--;
-		this.trySendOutbound();
-	},
-
-
-	canTrigger: function() {
-		return EventInstanceDataSource.prototype.canTrigger.apply(this, arguments) &&
-			!this.pendingSourceCnt;
-	}
-
-});

+ 159 - 0
src/models/RequestableEventDataSource.js

@@ -0,0 +1,159 @@
+
+var RequestableEventDataSource = EventDataSource.extend({
+
+	currentStart: null,
+	currentEnd: null,
+	currentTimezone: null,
+
+	requestsByUid: null,
+	pendingSourceCnt: 0,
+
+
+	constructor: function() {
+		EventDataSource.call(this);
+
+		this.requestsByUid = {};
+	},
+
+
+	request: function(start, end, timezone, force) {
+		if (
+			force ||
+			!this.currentStart || // first fetch?
+			this.currentTimezone !== timezone || // different timezone?
+			start.isBefore(this.currentStart) || // out of bounds?
+			end.isAfter(this.currentEnd)         // "
+		) {
+			this.currentTimezone = timezone;
+			this.currentStart = start;
+			this.currentEnd = end;
+			this.currentUnzonedRange = new UnzonedRange(
+				start.clone().stripZone(),
+				end.clone().stripZone()
+			);
+
+			this.refetchAllSources();
+		}
+	},
+
+
+	refetchSource: function(eventSource) {
+		if (this.currentUnzonedRange) {
+			this.freeze();
+			this.purgeSource(eventSource);
+			this.requestSource(eventSource);
+			this.thaw();
+		}
+	},
+
+
+	refetchAllSources: function() {
+		if (this.currentUnzonedRange) {
+			this.freeze();
+			this.purgeAllSources();
+			this.requestSources(this.getSources());
+			this.thaw();
+		}
+	},
+
+
+	getSources: function() {
+		return [];
+	},
+
+
+	requestSources: function(sources) {
+		this.freeze();
+
+		for (var i = 0; i < sources.length; i++) {
+			this.requestSource(sources[i]);
+		}
+
+		this.thaw();
+	},
+
+
+	requestSource: function(source) {
+		var _this = this;
+		var request = { source: source, status: 'pending' };
+
+		this.requestsByUid[source.uid] = request;
+		this.pendingSourceCnt += 1;
+
+		source.fetch(this.currentStart, this.currentEnd, this.currentTimezone).then(function(eventDefs) {
+			if (request.status !== 'cancelled') {
+				request.status = 'completed';
+				request.eventDefs = eventDefs;
+
+				_this.addEventDefs(eventDefs);
+				_this.reportSourceDone();
+			}
+		}, function() { // failure
+			if (request.status !== 'cancelled') {
+				request.status = 'failed';
+
+				_this.reportSourceDone();
+			}
+		});
+	},
+
+
+	purgeSource: function(source) {
+		var request = this.requestsByUid[source.uid];
+
+		if (request) {
+			delete this.requestsByUid[source.uid];
+
+			if (request.status === 'pending') {
+				request.status = 'cancelled';
+
+				this.reportSourceDone();
+			}
+			else if (request.status === 'completed') {
+				this.freeze();
+
+				request.eventDefs.forEach(this.removeEventDef.bind(this));
+
+				this.thaw();
+			}
+		}
+	},
+
+
+	purgeAllSources: function() {
+		var requestsByUid = this.requestsByUid;
+		var uid, request;
+		var completedCnt = 0;
+
+		for (uid in requestsByUid) {
+			request = requestsByUid[uid];
+
+			if (request.status === 'pending') {
+				request.status = 'cancelled';
+			}
+			else if (request.status === 'completed') {
+				completedCnt++;
+			}
+		}
+
+		this.pendingSourceCnt = 0;
+		this.requestsByUid = {};
+
+		if (completedCnt) {
+			this.removeAllEventDefs();
+		}
+	},
+
+
+	reportSourceDone: function() {
+		this.pendingSourceCnt--;
+		this.trySendOutbound();
+	},
+
+
+	canTrigger: function() {
+		return EventDataSource.prototype.canTrigger.apply(this, arguments) &&
+			!this.pendingSourceCnt;
+	}
+
+});