chuvsu-js 5.0.1 → 5.1.1
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/dist/lk/client.js +1 -2
- package/dist/tt/client.js +1 -2
- package/dist/tt/domain/index.d.ts +2 -0
- package/dist/tt/domain/index.js +1 -0
- package/dist/tt/domain/materialized-schedule.d.ts +40 -0
- package/dist/tt/domain/materialized-schedule.js +132 -0
- package/dist/tt/domain/schedule.d.ts +7 -0
- package/dist/tt/domain/schedule.js +32 -11
- package/docs/v5-migration.md +25 -0
- package/package.json +1 -1
package/dist/lk/client.js
CHANGED
|
@@ -55,8 +55,7 @@ export class StudentPortalClient {
|
|
|
55
55
|
}
|
|
56
56
|
isBinarySessionExpired(response) {
|
|
57
57
|
const prefix = response.body.subarray(0, 8_192).toString("utf8");
|
|
58
|
-
return this.isSessionExpired(prefix)
|
|
59
|
-
response.contentType?.toLowerCase().includes("text/html") === true;
|
|
58
|
+
return this.isSessionExpired(prefix);
|
|
60
59
|
}
|
|
61
60
|
async authGetBuffer(url) {
|
|
62
61
|
let res = await this.http.getBufferResponse(url);
|
package/dist/tt/client.js
CHANGED
|
@@ -191,8 +191,7 @@ export class TimetableClient {
|
|
|
191
191
|
}
|
|
192
192
|
isBinarySessionExpired(response) {
|
|
193
193
|
const prefix = response.body.subarray(0, 8_192).toString("utf8");
|
|
194
|
-
return this.isSessionExpired(prefix)
|
|
195
|
-
response.contentType?.toLowerCase().includes("text/html") === true;
|
|
194
|
+
return this.isSessionExpired(prefix);
|
|
196
195
|
}
|
|
197
196
|
async relogin() {
|
|
198
197
|
if (!this.loginMode)
|
|
@@ -3,5 +3,7 @@ export { TimetableDirectory } from "./directory.js";
|
|
|
3
3
|
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
|
4
4
|
export { MemoryTimetableRepositoryAdapter, TimetableRepository, } from "./repository.js";
|
|
5
5
|
export { Schedule } from "./schedule.js";
|
|
6
|
+
export { MaterializedSchedule, materializeSchedule, materializeScheduleSnapshot, } from "./materialized-schedule.js";
|
|
6
7
|
export type { ScheduleQueryOptions, ScheduleOptions, ScheduleWeekdayOptions, } from "./schedule.js";
|
|
8
|
+
export type { MaterializeScheduleOptions, MaterializedScheduleSnapshot, SerializedLessonOccurrence, SerializedLessonSourceRef, } from "./materialized-schedule.js";
|
|
7
9
|
export type { GroupAttendance, GroupRef, IngestResult, LessonId, LessonIdGenerator, LessonOccurrence, LessonRecurrence, LessonSeries, LessonSeriesId, RelationCompleteness, RelationSet, LessonSourceRef, LessonStatus, LessonSubstitution, LessonTransfer, NamedEntityRef, OccurrenceObservation, RoomRef, ScheduleObservation, ScheduleOwner, ScheduleSourceSnapshot, SeriesObservation, TeacherRef, TimetableDirectorySnapshot, TimetableRepositoryAdapter, TimetableRepositorySnapshot, } from "./types.js";
|
package/dist/tt/domain/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { TimetableDirectory } from "./directory.js";
|
|
|
3
3
|
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
|
4
4
|
export { MemoryTimetableRepositoryAdapter, TimetableRepository, } from "./repository.js";
|
|
5
5
|
export { Schedule } from "./schedule.js";
|
|
6
|
+
export { MaterializedSchedule, materializeSchedule, materializeScheduleSnapshot, } from "./materialized-schedule.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AcademicPeriod, LocalDate } from "../../common/types.js";
|
|
2
|
+
import type { Schedule } from "./schedule.js";
|
|
3
|
+
import type { LessonOccurrence, LessonSourceRef, ScheduleOwner } from "./types.js";
|
|
4
|
+
export interface SerializedLessonSourceRef extends Omit<LessonSourceRef, "observedAt"> {
|
|
5
|
+
observedAt: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SerializedLessonOccurrence extends Omit<LessonOccurrence, "sources"> {
|
|
8
|
+
sources: SerializedLessonSourceRef[];
|
|
9
|
+
}
|
|
10
|
+
export interface MaterializedScheduleSnapshot {
|
|
11
|
+
schemaVersion: 1;
|
|
12
|
+
repositoryRevision: number;
|
|
13
|
+
owner: ScheduleOwner;
|
|
14
|
+
academicYearStartYear: number;
|
|
15
|
+
period: AcademicPeriod;
|
|
16
|
+
lessonsByDate: Record<LocalDate, SerializedLessonOccurrence[]>;
|
|
17
|
+
}
|
|
18
|
+
export interface MaterializeScheduleOptions {
|
|
19
|
+
start?: Date;
|
|
20
|
+
end?: Date;
|
|
21
|
+
includeSources?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare class MaterializedSchedule {
|
|
24
|
+
readonly owner: ScheduleOwner;
|
|
25
|
+
readonly academicYearStartYear: number;
|
|
26
|
+
readonly period: AcademicPeriod;
|
|
27
|
+
readonly repositoryRevision: number;
|
|
28
|
+
private readonly lessonsByDate;
|
|
29
|
+
private readonly dateSetCache;
|
|
30
|
+
constructor(snapshot: MaterializedScheduleSnapshot);
|
|
31
|
+
export(): MaterializedScheduleSnapshot;
|
|
32
|
+
on(date: Date, options?: {
|
|
33
|
+
subgroup?: number;
|
|
34
|
+
}): LessonOccurrence[];
|
|
35
|
+
dateKeys(options?: {
|
|
36
|
+
subgroup?: number;
|
|
37
|
+
}): ReadonlySet<LocalDate>;
|
|
38
|
+
}
|
|
39
|
+
export declare function materializeSchedule(schedule: Schedule, options?: MaterializeScheduleOptions): MaterializedSchedule;
|
|
40
|
+
export declare function materializeScheduleSnapshot(schedule: Schedule, options?: MaterializeScheduleOptions): MaterializedScheduleSnapshot;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { formatLocalDate } from "../utils/index.js";
|
|
2
|
+
import { entityKey } from "./normalize.js";
|
|
3
|
+
function attendanceMatchesSubgroup(groups, owner, subgroup) {
|
|
4
|
+
if (subgroup == null)
|
|
5
|
+
return true;
|
|
6
|
+
if (owner.type === "group") {
|
|
7
|
+
const ownerKey = entityKey(owner.group);
|
|
8
|
+
return groups
|
|
9
|
+
.filter((value) => entityKey(value.group) === ownerKey)
|
|
10
|
+
.some((value) => value.subgroup == null || value.subgroup === subgroup);
|
|
11
|
+
}
|
|
12
|
+
return groups.some((value) => value.subgroup == null || value.subgroup === subgroup);
|
|
13
|
+
}
|
|
14
|
+
function serializeOccurrence(value, includeSources) {
|
|
15
|
+
return {
|
|
16
|
+
...structuredClone(value),
|
|
17
|
+
sources: includeSources
|
|
18
|
+
? value.sources.map((source) => ({
|
|
19
|
+
...structuredClone(source),
|
|
20
|
+
observedAt: source.observedAt.toISOString(),
|
|
21
|
+
}))
|
|
22
|
+
: [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function deserializeOccurrence(value) {
|
|
26
|
+
return {
|
|
27
|
+
...structuredClone(value),
|
|
28
|
+
sources: value.sources.map((source) => ({
|
|
29
|
+
...structuredClone(source),
|
|
30
|
+
observedAt: new Date(source.observedAt),
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function sortOccurrences(left, right) {
|
|
35
|
+
return (left.scheduledDate.localeCompare(right.scheduledDate) ||
|
|
36
|
+
((left.time?.start.hours ?? 24) * 60 +
|
|
37
|
+
(left.time?.start.minutes ?? 0)) -
|
|
38
|
+
((right.time?.start.hours ?? 24) * 60 +
|
|
39
|
+
(right.time?.start.minutes ?? 0)) ||
|
|
40
|
+
(left.slotNumber ?? Number.MAX_SAFE_INTEGER) -
|
|
41
|
+
(right.slotNumber ?? Number.MAX_SAFE_INTEGER) ||
|
|
42
|
+
left.subject.localeCompare(right.subject) ||
|
|
43
|
+
left.id.localeCompare(right.id));
|
|
44
|
+
}
|
|
45
|
+
export class MaterializedSchedule {
|
|
46
|
+
owner;
|
|
47
|
+
academicYearStartYear;
|
|
48
|
+
period;
|
|
49
|
+
repositoryRevision;
|
|
50
|
+
lessonsByDate = new Map();
|
|
51
|
+
dateSetCache = new Map();
|
|
52
|
+
constructor(snapshot) {
|
|
53
|
+
if (snapshot.schemaVersion !== 1) {
|
|
54
|
+
throw new Error(`Unsupported materialized schedule schema: ${snapshot.schemaVersion}`);
|
|
55
|
+
}
|
|
56
|
+
this.owner = structuredClone(snapshot.owner);
|
|
57
|
+
this.academicYearStartYear = snapshot.academicYearStartYear;
|
|
58
|
+
this.period = snapshot.period;
|
|
59
|
+
this.repositoryRevision = snapshot.repositoryRevision;
|
|
60
|
+
for (const [date, lessons] of Object.entries(snapshot.lessonsByDate)) {
|
|
61
|
+
this.lessonsByDate.set(date, lessons.map(deserializeOccurrence).sort(sortOccurrences));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export() {
|
|
65
|
+
return {
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
repositoryRevision: this.repositoryRevision,
|
|
68
|
+
owner: structuredClone(this.owner),
|
|
69
|
+
academicYearStartYear: this.academicYearStartYear,
|
|
70
|
+
period: this.period,
|
|
71
|
+
lessonsByDate: Object.fromEntries([...this.lessonsByDate].map(([date, lessons]) => [
|
|
72
|
+
date,
|
|
73
|
+
lessons.map((lesson) => serializeOccurrence(lesson, true)),
|
|
74
|
+
])),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
on(date, options) {
|
|
78
|
+
const lessons = this.lessonsByDate.get(formatLocalDate(date)) ?? [];
|
|
79
|
+
return structuredClone(options?.subgroup == null
|
|
80
|
+
? lessons
|
|
81
|
+
: lessons.filter((lesson) => attendanceMatchesSubgroup(lesson.groups.values, this.owner, options.subgroup)));
|
|
82
|
+
}
|
|
83
|
+
dateKeys(options) {
|
|
84
|
+
const subgroup = options?.subgroup;
|
|
85
|
+
const cached = this.dateSetCache.get(subgroup);
|
|
86
|
+
if (cached)
|
|
87
|
+
return new Set(cached);
|
|
88
|
+
const dates = new Set();
|
|
89
|
+
for (const [date, lessons] of this.lessonsByDate) {
|
|
90
|
+
if (subgroup == null ||
|
|
91
|
+
lessons.some((lesson) => attendanceMatchesSubgroup(lesson.groups.values, this.owner, subgroup))) {
|
|
92
|
+
dates.add(date);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.dateSetCache.set(subgroup, dates);
|
|
96
|
+
return new Set(dates);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function materializeSchedule(schedule, options) {
|
|
100
|
+
return new MaterializedSchedule(materializeScheduleSnapshot(schedule, options));
|
|
101
|
+
}
|
|
102
|
+
export function materializeScheduleSnapshot(schedule, options) {
|
|
103
|
+
const start = options?.start == null
|
|
104
|
+
? new Date(schedule.academicYearStartYear, 8, 1)
|
|
105
|
+
: new Date(options.start);
|
|
106
|
+
const end = options?.end == null
|
|
107
|
+
? new Date(schedule.academicYearStartYear + 1, 7, 31)
|
|
108
|
+
: new Date(options.end);
|
|
109
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime())) {
|
|
110
|
+
throw new RangeError("Materialized schedule range contains an invalid date");
|
|
111
|
+
}
|
|
112
|
+
if (start > end) {
|
|
113
|
+
throw new RangeError("Materialized schedule start must not exceed end");
|
|
114
|
+
}
|
|
115
|
+
const lessonsByDate = {};
|
|
116
|
+
const date = new Date(start);
|
|
117
|
+
date.setHours(0, 0, 0, 0);
|
|
118
|
+
for (; date <= end; date.setDate(date.getDate() + 1)) {
|
|
119
|
+
const lessons = schedule.on(date);
|
|
120
|
+
if (lessons.length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
lessonsByDate[formatLocalDate(date)] = lessons.map((lesson) => serializeOccurrence(lesson, options?.includeSources !== false));
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
repositoryRevision: schedule.revision,
|
|
127
|
+
owner: schedule.owner,
|
|
128
|
+
academicYearStartYear: schedule.academicYearStartYear,
|
|
129
|
+
period: schedule.period,
|
|
130
|
+
lessonsByDate,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AcademicPeriod } from "../../common/types.js";
|
|
2
2
|
import { type Holiday, type HolidayTransfer } from "../utils/index.js";
|
|
3
3
|
import { TimetableRepository } from "./repository.js";
|
|
4
|
+
import { type MaterializeScheduleOptions, type MaterializedSchedule, type MaterializedScheduleSnapshot } from "./materialized-schedule.js";
|
|
4
5
|
import type { LessonOccurrence, LessonSeries, ScheduleOwner } from "./types.js";
|
|
5
6
|
export interface ScheduleQueryOptions {
|
|
6
7
|
subgroup?: number;
|
|
@@ -20,10 +21,16 @@ export declare class Schedule {
|
|
|
20
21
|
readonly period: AcademicPeriod;
|
|
21
22
|
readonly holidays: Holiday[];
|
|
22
23
|
readonly holidayTransfers: HolidayTransfer[];
|
|
24
|
+
private cachedRevision;
|
|
25
|
+
private cachedSeries;
|
|
26
|
+
private cachedDirectOccurrences;
|
|
23
27
|
constructor(repository: TimetableRepository, owner: ScheduleOwner, academicYearStartYear: number, options?: ScheduleOptions);
|
|
24
28
|
get revision(): number;
|
|
29
|
+
private queryData;
|
|
25
30
|
series(): LessonSeries[];
|
|
26
31
|
on(date: Date, options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
32
|
+
materialize(options?: MaterializeScheduleOptions): MaterializedSchedule;
|
|
33
|
+
materializeSnapshot(options?: MaterializeScheduleOptions): MaterializedScheduleSnapshot;
|
|
27
34
|
week(week?: number, options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
28
35
|
weekday(weekday: number, options?: ScheduleWeekdayOptions): LessonOccurrence[];
|
|
29
36
|
today(options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
@@ -2,6 +2,7 @@ import { AcademicPeriod } from "../../common/types.js";
|
|
|
2
2
|
import { getAdjacentSemester, getCurrentPeriod, getMonday, getSemesterStart, getSemesterWeeks, getWeekNumber, formatLocalDate, isHoliday, parseLocalDate, RUSSIAN_HOLIDAYS, } from "../utils/index.js";
|
|
3
3
|
import { occurrenceIdForSeries } from "./ids.js";
|
|
4
4
|
import { entityKey, normalizeScheduleText } from "./normalize.js";
|
|
5
|
+
import { materializeSchedule, materializeScheduleSnapshot, } from "./materialized-schedule.js";
|
|
5
6
|
function dateAt(date, time) {
|
|
6
7
|
const value = new Date(date);
|
|
7
8
|
value.setHours(time.hours, time.minutes, 0, 0);
|
|
@@ -76,6 +77,9 @@ export class Schedule {
|
|
|
76
77
|
period;
|
|
77
78
|
holidays;
|
|
78
79
|
holidayTransfers;
|
|
80
|
+
cachedRevision = -1;
|
|
81
|
+
cachedSeries = [];
|
|
82
|
+
cachedDirectOccurrences = [];
|
|
79
83
|
constructor(repository, owner, academicYearStartYear, options) {
|
|
80
84
|
this.repository = repository;
|
|
81
85
|
this.owner = structuredClone(owner);
|
|
@@ -90,21 +94,32 @@ export class Schedule {
|
|
|
90
94
|
get revision() {
|
|
91
95
|
return this.repository.revision;
|
|
92
96
|
}
|
|
97
|
+
queryData() {
|
|
98
|
+
if (this.cachedRevision !== this.repository.revision) {
|
|
99
|
+
this.cachedSeries = this.repository.getSeries({
|
|
100
|
+
owner: this.owner,
|
|
101
|
+
academicYearStartYear: this.academicYearStartYear,
|
|
102
|
+
});
|
|
103
|
+
this.cachedDirectOccurrences = this.repository.getDirectOccurrences({
|
|
104
|
+
owner: this.owner,
|
|
105
|
+
academicYearStartYear: this.academicYearStartYear,
|
|
106
|
+
});
|
|
107
|
+
this.cachedRevision = this.repository.revision;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
series: this.cachedSeries,
|
|
111
|
+
direct: this.cachedDirectOccurrences,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
93
114
|
series() {
|
|
94
|
-
return this.
|
|
95
|
-
owner: this.owner,
|
|
96
|
-
academicYearStartYear: this.academicYearStartYear,
|
|
97
|
-
});
|
|
115
|
+
return structuredClone(this.queryData().series);
|
|
98
116
|
}
|
|
99
117
|
on(date, options) {
|
|
100
118
|
if (!this.isInAcademicYear(date))
|
|
101
119
|
return [];
|
|
102
120
|
const holiday = isHoliday(date, this.holidays, this.holidayTransfers);
|
|
103
121
|
const localDate = formatLocalDate(date);
|
|
104
|
-
const direct = this.
|
|
105
|
-
owner: this.owner,
|
|
106
|
-
academicYearStartYear: this.academicYearStartYear,
|
|
107
|
-
});
|
|
122
|
+
const { series: recurringSeries, direct } = this.queryData();
|
|
108
123
|
const movedSources = direct
|
|
109
124
|
.filter((value) => value.movedFrom)
|
|
110
125
|
.map((value) => ({
|
|
@@ -114,7 +129,7 @@ export class Schedule {
|
|
|
114
129
|
groups: value.groups.values,
|
|
115
130
|
}));
|
|
116
131
|
const occurrences = [];
|
|
117
|
-
for (const series of
|
|
132
|
+
for (const series of recurringSeries) {
|
|
118
133
|
if (holiday)
|
|
119
134
|
continue;
|
|
120
135
|
const week = recurrenceIncludes(series, date);
|
|
@@ -168,7 +183,7 @@ export class Schedule {
|
|
|
168
183
|
};
|
|
169
184
|
if (occurrenceMatchesOwner(occurrence, this.owner) &&
|
|
170
185
|
attendanceMatchesSubgroup(occurrence.groups.values, this.owner, options?.subgroup)) {
|
|
171
|
-
occurrences.push(occurrence);
|
|
186
|
+
occurrences.push(structuredClone(occurrence));
|
|
172
187
|
}
|
|
173
188
|
}
|
|
174
189
|
for (const occurrence of direct) {
|
|
@@ -179,10 +194,16 @@ export class Schedule {
|
|
|
179
194
|
if (!attendanceMatchesSubgroup(occurrence.groups.values, this.owner, options?.subgroup)) {
|
|
180
195
|
continue;
|
|
181
196
|
}
|
|
182
|
-
occurrences.push(occurrence);
|
|
197
|
+
occurrences.push(structuredClone(occurrence));
|
|
183
198
|
}
|
|
184
199
|
return occurrences.sort(sortOccurrences);
|
|
185
200
|
}
|
|
201
|
+
materialize(options) {
|
|
202
|
+
return materializeSchedule(this, options);
|
|
203
|
+
}
|
|
204
|
+
materializeSnapshot(options) {
|
|
205
|
+
return materializeScheduleSnapshot(this, options);
|
|
206
|
+
}
|
|
186
207
|
week(week, options) {
|
|
187
208
|
const period = this.period === AcademicPeriod.WinterSession ||
|
|
188
209
|
this.period === AcademicPeriod.SummerSession
|
package/docs/v5-migration.md
CHANGED
|
@@ -276,6 +276,31 @@ const client = new TimetableClient({
|
|
|
276
276
|
const snapshot = await client.exportRepository();
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
+
Репозиторий предназначен для серверного хранения и дополнения данных. Не
|
|
280
|
+
отправляйте полный `TimetableRepositorySnapshot` в браузер: его размер растет
|
|
281
|
+
вместе со всеми когда-либо загруженными расписаниями, а восстановление требует
|
|
282
|
+
повторной агрегации связей.
|
|
283
|
+
|
|
284
|
+
Для API, SSR и офлайн-кеша материализуйте только готовое расписание владельца:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
// Сервер: агрегация выполняется один раз, источники можно убрать из payload.
|
|
288
|
+
const payload = schedule.materializeSnapshot({ includeSources: false });
|
|
289
|
+
return Response.json(payload);
|
|
290
|
+
|
|
291
|
+
// Браузер: JSON восстанавливается в индексированное по дате представление.
|
|
292
|
+
import { MaterializedSchedule } from "chuvsu-js/browser";
|
|
293
|
+
|
|
294
|
+
const schedule = new MaterializedSchedule(await response.json());
|
|
295
|
+
schedule.on(date); // O(1) поиск даты
|
|
296
|
+
schedule.dateKeys({ subgroup: 2 }); // даты для календаря
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`MaterializedScheduleSnapshot` содержит уже объединенные `LessonOccurrence`,
|
|
300
|
+
но не канонический репозиторий. Поле `repositoryRevision` позволяет серверному
|
|
301
|
+
кешу инвалидировать снимок после дополнения пары из расписания преподавателя,
|
|
302
|
+
аудитории или другой группы.
|
|
303
|
+
|
|
279
304
|
Не передавайте экспорт `exportCache()` из v4 в `importCache()` v5: изменились
|
|
280
305
|
ключи, категории и форма закешированных страниц. Старый TTL-кеш и старые blob-
|
|
281
306
|
ключи следует удалить. Снимок репозитория v5 имеет `schemaVersion: 5`.
|