@forcecalendar/core 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/calendar/Calendar.js +96 -4
- package/core/events/Event.js +112 -0
- package/core/events/EventStore.js +176 -39
- package/core/events/RecurrenceEngine.js +218 -3
- package/core/events/RecurrenceEngineV2.js +103 -3
- package/core/index.js +1 -1
- package/core/types.js +43 -1
- package/package.json +1 -1
- package/types/calendar/Calendar.d.ts +60 -3
- package/types/events/Event.d.ts +36 -0
- package/types/events/EventStore.d.ts +50 -1
- package/types/events/RecurrenceEngine.d.ts +60 -1
- package/types/events/RecurrenceEngineV2.d.ts +45 -2
- package/types/index.d.ts +1 -1
- package/types/types.d.ts +121 -2
|
@@ -235,11 +235,103 @@ export class Calendar {
|
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
237
|
* Set all events (replaces existing)
|
|
238
|
-
*
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
*
|
|
239
|
+
* By default this clears the store and re-adds every entry, so every stored
|
|
240
|
+
* {@link Event} instance is replaced. Pass `{ reconcile: true }` to apply only
|
|
241
|
+
* the differences instead (see {@link Calendar#reconcileEvents}).
|
|
242
|
+
*
|
|
243
|
+
* Emits a single `eventsSet` event whose payload lists the resulting
|
|
244
|
+
* `events` plus the `added`, `updated`, `removed` and `unchanged` sets, so
|
|
245
|
+
* listeners can tell a snapshot load apart from user mutations (no
|
|
246
|
+
* `eventAdd`/`eventUpdate`/`eventRemove` events are emitted).
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* calendar.on('eventsSet', ({ added, updated, removed }) => {
|
|
250
|
+
* if (added.length || updated.length || removed.length) render();
|
|
251
|
+
* });
|
|
252
|
+
* calendar.setEvents(snapshot, { reconcile: true });
|
|
253
|
+
*
|
|
254
|
+
* @param {Array<import('../events/Event.js').Event|import('../types.js').EventData>} events - Array of events
|
|
255
|
+
* @param {import('../types.js').SetEventsOptions} [options={}] - Load options
|
|
256
|
+
* @returns {import('../types.js').EventsSetPayload} The applied change set
|
|
257
|
+
*/
|
|
258
|
+
setEvents(events, options = {}) {
|
|
259
|
+
if (options.reconcile) {
|
|
260
|
+
return this.reconcileEvents(events, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const removed = this.getEvents();
|
|
241
264
|
this.eventStore.loadEvents(events);
|
|
242
|
-
|
|
265
|
+
const added = this.getEvents();
|
|
266
|
+
|
|
267
|
+
/** @type {import('../types.js').EventsSetPayload} */
|
|
268
|
+
const payload = { events: added, added, updated: [], removed, unchanged: [] };
|
|
269
|
+
this._emit('eventsSet', payload);
|
|
270
|
+
return payload;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Reconcile the calendar with a snapshot of events, applying only the differences.
|
|
275
|
+
*
|
|
276
|
+
* Intended for consumers that receive periodic full snapshots (polling a
|
|
277
|
+
* server, a reactive `events` prop). Unchanged events keep their existing
|
|
278
|
+
* {@link Event} instance, changed ones are replaced, new ones are added and
|
|
279
|
+
* events missing from the snapshot are removed (unless
|
|
280
|
+
* `removeMissing: false`). Equivalence is decided by
|
|
281
|
+
* {@link Event.isEquivalent} unless an `isEquivalent` comparator is supplied.
|
|
282
|
+
* Plain event data without a `timeZone` defaults to the calendar timezone,
|
|
283
|
+
* exactly as with {@link Calendar#addEvent}.
|
|
284
|
+
*
|
|
285
|
+
* The store emits one `eventStoreChange` of type `batch` (or none when
|
|
286
|
+
* nothing differs) and the calendar emits a single `eventsSet` event
|
|
287
|
+
* carrying the change set. Per-event `eventAdd`/`eventUpdate`/`eventRemove`
|
|
288
|
+
* events are not emitted, so listeners that forward those to a backend are
|
|
289
|
+
* not triggered by a snapshot load.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* const { added, updated, removed, unchanged } = calendar.reconcileEvents(rows);
|
|
293
|
+
* updated.forEach(({ event, oldEvent }) => console.log(oldEvent.title, '->', event.title));
|
|
294
|
+
*
|
|
295
|
+
* @param {Array<import('../events/Event.js').Event|import('../types.js').EventData>} events - Complete snapshot of events
|
|
296
|
+
* @param {import('../types.js').ReconcileOptions} [options={}] - Reconcile options
|
|
297
|
+
* @returns {import('../types.js').EventsSetPayload} Resulting events and the applied change set
|
|
298
|
+
* @throws {Error} If an entry fails validation or two entries share an id
|
|
299
|
+
*/
|
|
300
|
+
reconcileEvents(events, options = {}) {
|
|
301
|
+
const { removeMissing, isEquivalent } = options;
|
|
302
|
+
const prepared = [];
|
|
303
|
+
for (const eventData of events) {
|
|
304
|
+
if (!(eventData instanceof Event) && eventData && !eventData.timeZone) {
|
|
305
|
+
prepared.push({ ...eventData, timeZone: this.config.timeZone });
|
|
306
|
+
} else {
|
|
307
|
+
prepared.push(eventData);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const storeOptions = {};
|
|
312
|
+
if (removeMissing !== undefined) storeOptions.removeMissing = removeMissing;
|
|
313
|
+
if (isEquivalent !== undefined) storeOptions.isEquivalent = isEquivalent;
|
|
314
|
+
|
|
315
|
+
const result = this.eventStore.reconcile(prepared, storeOptions);
|
|
316
|
+
|
|
317
|
+
/** @type {import('../types.js').EventsSetPayload} */
|
|
318
|
+
const payload = { events: this.getEvents(), ...result };
|
|
319
|
+
this._emit('eventsSet', payload);
|
|
320
|
+
return payload;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get the event store's change counter
|
|
325
|
+
*
|
|
326
|
+
* The counter increases with every add/update/remove/clear and every
|
|
327
|
+
* committed batch, so comparing two readings is a cheap way to find out
|
|
328
|
+
* whether {@link Calendar#setEvents} or {@link Calendar#reconcileEvents}
|
|
329
|
+
* changed anything.
|
|
330
|
+
*
|
|
331
|
+
* @returns {number} Current store version
|
|
332
|
+
*/
|
|
333
|
+
getEventsVersion() {
|
|
334
|
+
return this.eventStore.version;
|
|
243
335
|
}
|
|
244
336
|
|
|
245
337
|
/**
|
package/core/events/Event.js
CHANGED
|
@@ -6,6 +6,42 @@
|
|
|
6
6
|
|
|
7
7
|
import { TimezoneManager } from '../timezone/TimezoneManager.js';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Structural equality for plain event sub-values (attendees, metadata, rules...).
|
|
11
|
+
* Handles primitives, Dates, arrays (order-sensitive) and plain objects
|
|
12
|
+
* (key-order insensitive). Functions and symbols never compare equal unless
|
|
13
|
+
* they are the same reference.
|
|
14
|
+
* @param {*} a
|
|
15
|
+
* @param {*} b
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
function deepEqual(a, b) {
|
|
19
|
+
if (a === b) return true;
|
|
20
|
+
if (a == null || b == null) return false;
|
|
21
|
+
if (typeof a !== 'object' || typeof b !== 'object') {
|
|
22
|
+
// NaN is the only primitive that is not === itself
|
|
23
|
+
return Number.isNaN(a) && Number.isNaN(b);
|
|
24
|
+
}
|
|
25
|
+
if (a instanceof Date || b instanceof Date) {
|
|
26
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
29
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
30
|
+
for (let i = 0; i < a.length; i++) {
|
|
31
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const keysA = Object.keys(a).filter(k => a[k] !== undefined);
|
|
36
|
+
const keysB = Object.keys(b).filter(k => b[k] !== undefined);
|
|
37
|
+
if (keysA.length !== keysB.length) return false;
|
|
38
|
+
for (const key of keysA) {
|
|
39
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
40
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
9
45
|
export class Event {
|
|
10
46
|
// Field size limits
|
|
11
47
|
static FIELD_LIMITS = {
|
|
@@ -609,6 +645,82 @@ export class Event {
|
|
|
609
645
|
);
|
|
610
646
|
}
|
|
611
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Fields compared by {@link Event.isEquivalent}, in comparison order.
|
|
650
|
+
* Scalars are compared with strict equality, dates by timestamp and
|
|
651
|
+
* structured fields (recurrence rule, organizer, attendees, reminders,
|
|
652
|
+
* categories, attachments, conference data, metadata) structurally.
|
|
653
|
+
* @type {ReadonlyArray<string>}
|
|
654
|
+
*/
|
|
655
|
+
static EQUIVALENCE_FIELDS = Object.freeze([
|
|
656
|
+
'id',
|
|
657
|
+
'title',
|
|
658
|
+
'start',
|
|
659
|
+
'end',
|
|
660
|
+
'allDay',
|
|
661
|
+
'timeZone',
|
|
662
|
+
'endTimeZone',
|
|
663
|
+
'description',
|
|
664
|
+
'location',
|
|
665
|
+
'color',
|
|
666
|
+
'backgroundColor',
|
|
667
|
+
'borderColor',
|
|
668
|
+
'textColor',
|
|
669
|
+
'recurring',
|
|
670
|
+
'recurrenceRule',
|
|
671
|
+
'status',
|
|
672
|
+
'visibility',
|
|
673
|
+
'organizer',
|
|
674
|
+
'attendees',
|
|
675
|
+
'reminders',
|
|
676
|
+
'categories',
|
|
677
|
+
'attachments',
|
|
678
|
+
'conferenceData',
|
|
679
|
+
'metadata'
|
|
680
|
+
]);
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Deep equivalence check over the full event data surface.
|
|
684
|
+
*
|
|
685
|
+
* Unlike {@link Event#equals} (which only looks at identity, title, dates,
|
|
686
|
+
* description, location, recurrence and status) this compares every field
|
|
687
|
+
* that can be supplied through {@link EventData}: timezones, all-day flag,
|
|
688
|
+
* colours, visibility, organizer, attendees, reminders, categories,
|
|
689
|
+
* attachments, conference data and metadata. Dates are compared by
|
|
690
|
+
* timestamp; structured fields are compared structurally (arrays are
|
|
691
|
+
* order-sensitive, object key order is ignored). Plain event data objects
|
|
692
|
+
* are normalized through the {@link Event} constructor before comparison so
|
|
693
|
+
* that `{ color: 'red' }` and `{ backgroundColor: 'red', borderColor: 'red' }`
|
|
694
|
+
* describe the same event. Two events with different ids are never
|
|
695
|
+
* equivalent.
|
|
696
|
+
*
|
|
697
|
+
* This is the default comparator used by `EventStore.reconcile()` to decide
|
|
698
|
+
* whether an incoming snapshot entry replaces the stored event.
|
|
699
|
+
*
|
|
700
|
+
* @example
|
|
701
|
+
* Event.isEquivalent(stored, { ...stored.toObject(), backgroundColor: '#f00' }); // false
|
|
702
|
+
* Event.isEquivalent(stored, stored.clone()); // true
|
|
703
|
+
*
|
|
704
|
+
* @param {Event|import('../types.js').EventData} a - First event or raw event data
|
|
705
|
+
* @param {Event|import('../types.js').EventData} b - Second event or raw event data
|
|
706
|
+
* @returns {boolean} True when both describe the same event data
|
|
707
|
+
* @throws {Error} If raw event data fails {@link Event.validate}
|
|
708
|
+
*/
|
|
709
|
+
static isEquivalent(a, b) {
|
|
710
|
+
if (a === b) return true;
|
|
711
|
+
if (a == null || b == null) return false;
|
|
712
|
+
|
|
713
|
+
const left = a instanceof Event ? a : new Event(a);
|
|
714
|
+
const right = b instanceof Event ? b : new Event(b);
|
|
715
|
+
|
|
716
|
+
for (const field of Event.EQUIVALENCE_FIELDS) {
|
|
717
|
+
if (!deepEqual(left[field], right[field])) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
612
724
|
// ============ Attendee Management Methods ============
|
|
613
725
|
|
|
614
726
|
/**
|
|
@@ -86,19 +86,11 @@ export class EventStore {
|
|
|
86
86
|
this._indexEvent(event);
|
|
87
87
|
|
|
88
88
|
// Notify listeners (batch if in batch mode)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
} else {
|
|
96
|
-
this._notifyChange({
|
|
97
|
-
type: 'add',
|
|
98
|
-
event,
|
|
99
|
-
version: ++this.version
|
|
100
|
-
});
|
|
101
|
-
}
|
|
89
|
+
this._queueChange({
|
|
90
|
+
type: 'add',
|
|
91
|
+
event,
|
|
92
|
+
version: ++this.version
|
|
93
|
+
});
|
|
102
94
|
|
|
103
95
|
return event;
|
|
104
96
|
});
|
|
@@ -117,24 +109,10 @@ export class EventStore {
|
|
|
117
109
|
throw new Error(`Event with id ${eventId} not found`);
|
|
118
110
|
}
|
|
119
111
|
|
|
120
|
-
// Remove old indices
|
|
121
|
-
this._unindexEvent(existingEvent);
|
|
122
|
-
|
|
123
112
|
// Create updated event
|
|
124
113
|
const updatedEvent = existingEvent.clone(updates);
|
|
125
114
|
|
|
126
|
-
|
|
127
|
-
this.events.set(eventId, updatedEvent);
|
|
128
|
-
|
|
129
|
-
// Update cache with new event data
|
|
130
|
-
this.optimizer.cache(eventId, updatedEvent, 'event');
|
|
131
|
-
|
|
132
|
-
// Clear query and date range caches since results may have changed
|
|
133
|
-
this.optimizer.queryCache.clear();
|
|
134
|
-
this.optimizer.dateRangeCache.clear();
|
|
135
|
-
|
|
136
|
-
// Re-index
|
|
137
|
-
this._indexEvent(updatedEvent);
|
|
115
|
+
this._replaceEvent(existingEvent, updatedEvent);
|
|
138
116
|
|
|
139
117
|
// Notify listeners
|
|
140
118
|
this._notifyChange({
|
|
@@ -147,6 +125,31 @@ export class EventStore {
|
|
|
147
125
|
return updatedEvent;
|
|
148
126
|
}
|
|
149
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Swap a stored event for a new instance with the same id, keeping
|
|
130
|
+
* indices and caches in sync. Does not notify listeners.
|
|
131
|
+
* @param {Event} existingEvent - Event currently in the store
|
|
132
|
+
* @param {Event} replacement - Event instance that takes its place
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
_replaceEvent(existingEvent, replacement) {
|
|
136
|
+
// Remove old indices
|
|
137
|
+
this._unindexEvent(existingEvent);
|
|
138
|
+
|
|
139
|
+
// Store replacement
|
|
140
|
+
this.events.set(replacement.id, replacement);
|
|
141
|
+
|
|
142
|
+
// Update cache with new event data
|
|
143
|
+
this.optimizer.cache(replacement.id, replacement, 'event');
|
|
144
|
+
|
|
145
|
+
// Clear query and date range caches since results may have changed
|
|
146
|
+
this.optimizer.queryCache.clear();
|
|
147
|
+
this.optimizer.dateRangeCache.clear();
|
|
148
|
+
|
|
149
|
+
// Re-index
|
|
150
|
+
this._indexEvent(replacement);
|
|
151
|
+
}
|
|
152
|
+
|
|
150
153
|
/**
|
|
151
154
|
* Remove an event from the store
|
|
152
155
|
* @param {string} eventId - The event ID to remove
|
|
@@ -158,16 +161,7 @@ export class EventStore {
|
|
|
158
161
|
return false;
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
this.events.delete(eventId);
|
|
163
|
-
|
|
164
|
-
// Invalidate caches
|
|
165
|
-
this.optimizer.eventCache.delete(eventId);
|
|
166
|
-
this.optimizer.queryCache.clear();
|
|
167
|
-
this.optimizer.dateRangeCache.clear();
|
|
168
|
-
|
|
169
|
-
// Remove from indices
|
|
170
|
-
this._unindexEvent(event);
|
|
164
|
+
this._detachEvent(event);
|
|
171
165
|
|
|
172
166
|
// Notify listeners
|
|
173
167
|
this._notifyChange({
|
|
@@ -179,6 +173,24 @@ export class EventStore {
|
|
|
179
173
|
return true;
|
|
180
174
|
}
|
|
181
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Remove an event from storage, caches and indices. Does not notify listeners.
|
|
178
|
+
* @param {Event} event - Event currently in the store
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
_detachEvent(event) {
|
|
182
|
+
// Remove from primary storage
|
|
183
|
+
this.events.delete(event.id);
|
|
184
|
+
|
|
185
|
+
// Invalidate caches
|
|
186
|
+
this.optimizer.eventCache.delete(event.id);
|
|
187
|
+
this.optimizer.queryCache.clear();
|
|
188
|
+
this.optimizer.dateRangeCache.clear();
|
|
189
|
+
|
|
190
|
+
// Remove from indices
|
|
191
|
+
this._unindexEvent(event);
|
|
192
|
+
}
|
|
193
|
+
|
|
182
194
|
/**
|
|
183
195
|
* Get an event by ID
|
|
184
196
|
* @param {string} eventId - The event ID
|
|
@@ -705,6 +717,118 @@ export class EventStore {
|
|
|
705
717
|
this.commitBatch();
|
|
706
718
|
}
|
|
707
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Reconcile the store with a snapshot of events, applying only the differences.
|
|
722
|
+
*
|
|
723
|
+
* Compared with {@link EventStore#loadEvents} (clear + re-add everything) this:
|
|
724
|
+
* - keeps the existing {@link Event} instance for every entry that is
|
|
725
|
+
* equivalent to the stored one (identity is preserved, no notification),
|
|
726
|
+
* - replaces stored events whose incoming data differs (`update` change),
|
|
727
|
+
* - adds events whose id is not in the store (`add` change),
|
|
728
|
+
* - removes stored events missing from the snapshot (`remove` change),
|
|
729
|
+
* unless `removeMissing` is `false`,
|
|
730
|
+
* - emits a single `batch` notification listing those changes, or nothing at
|
|
731
|
+
* all when the snapshot matches the store. When called while a batch is
|
|
732
|
+
* already open the changes are queued on that batch instead.
|
|
733
|
+
*
|
|
734
|
+
* Input is validated up front: invalid event data or duplicate ids throw
|
|
735
|
+
* before the store is modified. Any error raised while applying the diff
|
|
736
|
+
* rolls the store back to its previous state.
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* // periodic server snapshot
|
|
740
|
+
* const { added, updated, removed } = store.reconcile(rowsFromServer);
|
|
741
|
+
* if (added.length || updated.length || removed.length) rerender();
|
|
742
|
+
*
|
|
743
|
+
* @param {Array<Event|import('../types.js').EventData>} events - Complete snapshot of events
|
|
744
|
+
* @param {import('../types.js').ReconcileOptions} [options={}] - Reconcile options
|
|
745
|
+
* @returns {import('../types.js').ReconcileResult} Events that were added, updated, removed and left untouched
|
|
746
|
+
* @throws {Error} If an entry fails validation or two entries share an id
|
|
747
|
+
*/
|
|
748
|
+
reconcile(events, options = {}) {
|
|
749
|
+
const { removeMissing = true, isEquivalent = Event.isEquivalent } = options;
|
|
750
|
+
|
|
751
|
+
if (!events || typeof events[Symbol.iterator] !== 'function') {
|
|
752
|
+
throw new Error('reconcile() expects an iterable of events');
|
|
753
|
+
}
|
|
754
|
+
if (typeof isEquivalent !== 'function') {
|
|
755
|
+
throw new Error('reconcile() option isEquivalent must be a function');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return this.optimizer.measure('reconcile', () => {
|
|
759
|
+
// Normalize and validate everything before touching the store
|
|
760
|
+
/** @type {Map<string, Event>} */
|
|
761
|
+
const incoming = new Map();
|
|
762
|
+
for (const eventData of events) {
|
|
763
|
+
const event = eventData instanceof Event ? eventData : new Event(eventData);
|
|
764
|
+
if (incoming.has(event.id)) {
|
|
765
|
+
throw new Error(`Duplicate event id in reconcile input: ${event.id}`);
|
|
766
|
+
}
|
|
767
|
+
incoming.set(event.id, event);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** @type {import('../types.js').ReconcileResult} */
|
|
771
|
+
const result = { added: [], updated: [], removed: [], unchanged: [] };
|
|
772
|
+
|
|
773
|
+
// Nest inside an existing batch if one is open, otherwise own one
|
|
774
|
+
const ownsBatch = !this.isBatchMode;
|
|
775
|
+
if (ownsBatch) {
|
|
776
|
+
this.startBatch(true);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
if (removeMissing) {
|
|
781
|
+
for (const existing of Array.from(this.events.values())) {
|
|
782
|
+
if (!incoming.has(existing.id)) {
|
|
783
|
+
this._detachEvent(existing);
|
|
784
|
+
this._queueChange({ type: 'remove', event: existing, version: ++this.version });
|
|
785
|
+
result.removed.push(existing);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
for (const event of incoming.values()) {
|
|
791
|
+
const existing = this.events.get(event.id);
|
|
792
|
+
if (!existing) {
|
|
793
|
+
this.events.set(event.id, event);
|
|
794
|
+
this.optimizer.cache(event.id, event, 'event');
|
|
795
|
+
this._indexEvent(event);
|
|
796
|
+
this._queueChange({ type: 'add', event, version: ++this.version });
|
|
797
|
+
result.added.push(event);
|
|
798
|
+
} else if (existing === event || isEquivalent(existing, event)) {
|
|
799
|
+
result.unchanged.push(existing);
|
|
800
|
+
} else {
|
|
801
|
+
this._replaceEvent(existing, event);
|
|
802
|
+
this._queueChange({
|
|
803
|
+
type: 'update',
|
|
804
|
+
event,
|
|
805
|
+
oldEvent: existing,
|
|
806
|
+
version: ++this.version
|
|
807
|
+
});
|
|
808
|
+
result.updated.push({ event, oldEvent: existing });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (result.added.length > 0) {
|
|
813
|
+
// Newly indexed events may change range/query results
|
|
814
|
+
this.optimizer.queryCache.clear();
|
|
815
|
+
this.optimizer.dateRangeCache.clear();
|
|
816
|
+
}
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (ownsBatch) {
|
|
819
|
+
this.rollbackBatch();
|
|
820
|
+
}
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (ownsBatch) {
|
|
825
|
+
this.commitBatch();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return result;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
708
832
|
/**
|
|
709
833
|
* Subscribe to store changes
|
|
710
834
|
* @param {Function} callback - Callback function
|
|
@@ -949,6 +1073,19 @@ export class EventStore {
|
|
|
949
1073
|
* Notify listeners of changes
|
|
950
1074
|
* @private
|
|
951
1075
|
*/
|
|
1076
|
+
/**
|
|
1077
|
+
* Deliver a change now, or queue it when a batch is open
|
|
1078
|
+
* @param {import('../types.js').EventStoreChange} change - Change to deliver
|
|
1079
|
+
* @private
|
|
1080
|
+
*/
|
|
1081
|
+
_queueChange(change) {
|
|
1082
|
+
if (this.isBatchMode) {
|
|
1083
|
+
this.batchNotifications.push(change);
|
|
1084
|
+
} else {
|
|
1085
|
+
this._notifyChange(change);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
952
1089
|
_notifyChange(change) {
|
|
953
1090
|
for (const listener of this.listeners) {
|
|
954
1091
|
try {
|
|
@@ -1066,7 +1203,7 @@ export class EventStore {
|
|
|
1066
1203
|
this.batchBackup = null;
|
|
1067
1204
|
|
|
1068
1205
|
// Clear cache
|
|
1069
|
-
this.
|
|
1206
|
+
this.clearCaches();
|
|
1070
1207
|
}
|
|
1071
1208
|
|
|
1072
1209
|
this.batchNotifications = [];
|