Skip to content

Commit 7caba23

Browse files
authored
Scheduler: Add timezones cache (T1296816) (DevExpress#30239)
Co-authored-by: Vladimir Bushmanov <[email protected]>
1 parent 8b41dd0 commit 7caba23

22 files changed

+314
-573
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
import {
3+
describe, expect, it, jest,
4+
} from '@jest/globals';
5+
6+
import Scheduler from '../m_scheduler';
7+
import timezoneUtils from '../m_utils_time_zone';
8+
9+
const startDate = new Date(2025, 0, 6);
10+
const delta = 15 * 60 * 1000;
11+
const dataSource = Array.from({ length: 10 }, (_, i) => ({
12+
startDate: startDate.getTime() + i * delta,
13+
endDate: startDate.getTime() + (i + 1) * delta,
14+
recurrenceRule: 'FREQ=DAILY;INTERVAL=7',
15+
text: `Appointment ${i + 1}`,
16+
}));
17+
18+
describe('scheduler', () => {
19+
it.each([
20+
{ timeZone: 'Europe/London' },
21+
{ timeZone: undefined },
22+
])('should memo Intl object for timezone: $timeZone', async ({ timeZone }) => {
23+
const container = document.createElement('div');
24+
const scheduler = new Scheduler(container, {
25+
dataSource,
26+
timeZone,
27+
views: ['week'],
28+
currentView: 'week',
29+
currentDate: new Date(2025, 0, 8, 15),
30+
startDayHour: 8,
31+
firstDayOfWeek: 1,
32+
height: 600,
33+
});
34+
await timezoneUtils.cacheTimeZones();
35+
36+
expect(container.classList).toContain('dx-scheduler');
37+
38+
jest.spyOn(Intl, 'DateTimeFormat');
39+
40+
const navigator = container.querySelector('.dx-scheduler-header .dx-scheduler-navigator') as HTMLDivElement;
41+
const nextButton = navigator.querySelector('.dx-scheduler-navigator-next') as HTMLDivElement;
42+
nextButton.click();
43+
expect(navigator.querySelector('.dx-scheduler-navigator-caption')?.textContent).toBe('13-19 January 2025');
44+
nextButton.click();
45+
expect(navigator.querySelector('.dx-scheduler-navigator-caption')?.textContent).toBe('20-26 January 2025');
46+
47+
expect(Intl.DateTimeFormat).toHaveBeenCalledTimes(0);
48+
});
49+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
describe, expect, it, jest,
3+
} from '@jest/globals';
4+
5+
import { getResourceManagerMock } from '../__mock__/resourceManager.mock';
6+
import SchedulerTimelineDay from '../workspaces/m_timeline_day';
7+
import SchedulerTimelineMonth from '../workspaces/m_timeline_month';
8+
import SchedulerTimelineWeek from '../workspaces/m_timeline_week';
9+
import SchedulerTimelineWorkWeek from '../workspaces/m_timeline_work_week';
10+
import type SchedulerWorkSpace from '../workspaces/m_work_space';
11+
import SchedulerWorkSpaceDay from '../workspaces/m_work_space_day';
12+
import SchedulerWorkSpaceMonth from '../workspaces/m_work_space_month';
13+
import SchedulerWorkSpaceWeek from '../workspaces/m_work_space_week';
14+
import SchedulerWorkSpaceWorkWeek from '../workspaces/m_work_space_work_week';
15+
16+
type WorkspaceConstructor<T> = new (container: Element, options?: any) => T;
17+
18+
const createWorkspace = <T extends SchedulerWorkSpace>(
19+
WorkSpace: WorkspaceConstructor<T>,
20+
currentView: string,
21+
): T => {
22+
const container = document.createElement('div');
23+
const workspace = new WorkSpace(container, {
24+
views: [currentView],
25+
currentView,
26+
currentDate: new Date(2017, 4, 25),
27+
firstDayOfWeek: 0,
28+
getResourceManager: () => getResourceManagerMock([]),
29+
});
30+
(workspace as any)._isVisible = () => true;
31+
expect(container.classList).toContain('dx-scheduler-work-space');
32+
33+
return workspace;
34+
};
35+
const workSpaces: {
36+
currentView: string;
37+
WorkSpace: WorkspaceConstructor<SchedulerWorkSpace>;
38+
}[] = [
39+
{ currentView: 'day', WorkSpace: SchedulerWorkSpaceDay },
40+
{ currentView: 'week', WorkSpace: SchedulerWorkSpaceWeek },
41+
{ currentView: 'workWeek', WorkSpace: SchedulerWorkSpaceWorkWeek },
42+
{ currentView: 'month', WorkSpace: SchedulerWorkSpaceMonth },
43+
{ currentView: 'timelineDay', WorkSpace: SchedulerTimelineDay },
44+
{ currentView: 'timelineWeek', WorkSpace: SchedulerTimelineWeek },
45+
{ currentView: 'timelineWorkWeek', WorkSpace: SchedulerTimelineWorkWeek },
46+
{ currentView: 'timelineMonth', WorkSpace: SchedulerTimelineMonth },
47+
];
48+
49+
describe('scheduler workspace', () => {
50+
workSpaces.forEach(({ currentView, WorkSpace }) => {
51+
it(`should clear cache on dimension change, view: ${currentView}`, () => {
52+
const workspace = createWorkspace(WorkSpace, currentView);
53+
jest.spyOn(workspace.cache, 'clear');
54+
55+
workspace.cache.memo('test', () => 'value');
56+
workspace._dimensionChanged();
57+
58+
expect(workspace.cache.clear).toHaveBeenCalledTimes(1);
59+
});
60+
61+
it(`should clear cache on _cleanView call, view: ${currentView}`, () => {
62+
const workspace = createWorkspace(WorkSpace, currentView);
63+
jest.spyOn(workspace.cache, 'clear');
64+
65+
workspace.cache.memo('test', () => 'value');
66+
workspace._cleanView();
67+
68+
expect(workspace.cache.clear).toHaveBeenCalledTimes(1);
69+
expect(workspace.cache.size).toBe(0);
70+
});
71+
});
72+
});

packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,6 @@ class SchedulerAppointments extends CollectionWidget {
580580
const allowResize = this.option('allowResize') && (!isDefined(settings.skipResizing) || isString(settings.skipResizing));
581581
const allowDrag = this.option('allowDrag');
582582
const { allDay } = settings;
583-
this.invoke('setCellDataCacheAlias', this._currentAppointmentSettings, geometry);
584583

585584
if (settings.virtual) {
586585
const appointmentConfig = {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
describe, expect, it, jest,
3+
} from '@jest/globals';
4+
5+
import { Cache } from './global_cache';
6+
7+
describe('global cache', () => {
8+
it('should be empty at initialization', () => {
9+
const cache = new Cache();
10+
expect(cache.size).toBe(0);
11+
});
12+
13+
it('should get non-existed value', () => {
14+
const cache = new Cache();
15+
16+
expect(cache.get('test0')).toBe(undefined);
17+
});
18+
19+
it('should get existed value', () => {
20+
const cache = new Cache();
21+
cache.memo('test0', () => 'callbackValue');
22+
23+
expect(cache.get('test0')).toBe('callbackValue');
24+
});
25+
26+
it('should memo value', () => {
27+
const cache = new Cache();
28+
const valueCallback = jest.fn().mockReturnValue(1).mockReturnValueOnce(2);
29+
const memoValue = cache.memo('test', valueCallback);
30+
31+
expect(cache.get('test')).toBe(memoValue);
32+
expect(cache.size).toBe(1);
33+
});
34+
35+
it('should memo twice for deleted value', () => {
36+
const cache = new Cache();
37+
const valueCallback1 = jest.fn().mockReturnValue(1).mockReturnValueOnce(2);
38+
const valueCallback2 = jest.fn().mockReturnValue(1).mockReturnValueOnce(2);
39+
const memoValue1 = cache.memo('test1', valueCallback1);
40+
const memoValue2 = cache.memo('test2', valueCallback2);
41+
cache.delete('test1');
42+
43+
expect(cache.size).toBe(1);
44+
expect(cache.memo('test1', valueCallback1)).not.toBe(memoValue1);
45+
expect(cache.memo('test2', valueCallback2)).toBe(memoValue2);
46+
expect(cache.size).toBe(2);
47+
});
48+
49+
it('should delete existed value', () => {
50+
const cache = new Cache();
51+
cache.memo('test1', () => 'callbackValue1');
52+
cache.delete('test1');
53+
54+
expect(cache.get('test1')).toBe(undefined);
55+
expect(cache.size).toBe(0);
56+
});
57+
58+
it('should delete non-existed value', () => {
59+
const cache = new Cache();
60+
cache.memo('test1', () => 'callbackValue1');
61+
cache.memo('test2', () => 'callbackValue2');
62+
cache.delete('non-existed');
63+
64+
expect(cache.size).toBe(2);
65+
});
66+
67+
it('should clear all values', () => {
68+
const cache = new Cache();
69+
cache.memo('test0', () => 'callbackValue');
70+
cache.memo('test1', () => 'callbackValue');
71+
cache.clear();
72+
73+
expect(cache.size).toBe(0);
74+
});
75+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { isDefined } from '@js/core/utils/type';
2+
3+
export class Cache {
4+
private readonly cache = new Map();
5+
6+
get size(): number { return this.cache.size; }
7+
8+
clear(): void {
9+
this.cache.clear();
10+
}
11+
12+
get<R>(name: string): R | undefined {
13+
return this.cache.get(name) as R | undefined;
14+
}
15+
16+
memo<R>(name: string, valueCallback: () => R): R {
17+
if (!this.cache.has(name)) {
18+
const value = valueCallback();
19+
20+
if (isDefined(value)) {
21+
this.cache.set(name, value);
22+
}
23+
}
24+
25+
return this.cache.get(name) as R;
26+
}
27+
28+
delete(name: string): void {
29+
this.cache.delete(name);
30+
}
31+
}
32+
33+
export const globalCache = {
34+
timezones: new Cache(),
35+
};

packages/devextreme/js/__internal/scheduler/m_recurrence.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class RecurrenceProcessor {
121121

122122
_getLocalMachineOffset(rruleDate) {
123123
const machineTimezoneOffset = timeZoneUtils.getClientTimezoneOffset(rruleDate);
124-
const machineTimezoneName = dateUtils.getMachineTimezoneName();
124+
const machineTimezoneName = timeZoneUtils.getMachineTimezoneName();
125125
const result = [machineTimezoneOffset];
126126

127127
// NOTE: Workaround for the RRule bug with timezones greater than GMT+12 (e.g. Apia Standard Time GMT+13)

packages/devextreme/js/__internal/scheduler/m_subscribes.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ const subscribes = {
3535
return this.isVirtualScrolling();
3636
},
3737

38-
setCellDataCacheAlias(appointment, geometry) {
39-
this._workSpace.setCellDataCacheAlias(appointment, geometry);
40-
},
41-
4238
isGroupedByDate() {
4339
return this.getWorkSpace().isGroupedByDate();
4440
},

packages/devextreme/js/__internal/scheduler/m_utils_time_zone.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,35 @@ import {
33
} from '@jest/globals';
44
import { macroTaskArray } from '@ts/scheduler/utils/index';
55

6+
import { globalCache } from './global_cache';
67
import timeZoneUtils from './m_utils_time_zone';
78
import timeZoneList from './timezones/timezone_list';
89

910
const defaultTimeZones = timeZoneList.value;
1011

1112
describe('timezone utils', () => {
13+
beforeAll(() => {
14+
globalCache.timezones.clear();
15+
});
16+
17+
describe('calculateTimezoneByValue', () => {
18+
it('should work faster after first run', () => {
19+
let now = Date.now();
20+
timeZoneList.value.forEach((timezone) => {
21+
timeZoneUtils.calculateTimezoneByValue(timezone);
22+
});
23+
const delta1 = Date.now() - now; // 41
24+
now = Date.now();
25+
timeZoneList.value.forEach((timezone) => {
26+
timeZoneUtils.calculateTimezoneByValue(timezone);
27+
});
28+
const delta2 = Date.now() - now; // 6
29+
30+
expect(globalCache.timezones.size).toBe(timeZoneList.value.length);
31+
expect(delta2).toBeLessThan(delta1 / 5);
32+
});
33+
});
34+
1235
describe('cacheTimeZones / getTimeZonesCache', () => {
1336
beforeAll(() => {
1437
timeZoneList.value = [

0 commit comments

Comments
 (0)