chuvsu-js 5.2.2 → 5.3.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/tt/domain/ids.d.ts +2 -0
- package/dist/tt/domain/ids.js +39 -9
- package/dist/tt/domain/index.d.ts +3 -3
- package/dist/tt/domain/index.js +2 -2
- package/dist/tt/domain/schedule-snapshot.d.ts +51 -0
- package/dist/tt/domain/schedule-snapshot.js +103 -0
- package/dist/tt/domain/schedule.d.ts +7 -6
- package/dist/tt/domain/schedule.js +11 -12
- package/docs/v5-migration.md +17 -11
- package/package.json +1 -1
- package/dist/tt/domain/materialized-schedule.d.ts +0 -40
- package/dist/tt/domain/materialized-schedule.js +0 -132
package/dist/tt/domain/ids.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export declare class RandomLessonIdGenerator implements LessonIdGenerator {
|
|
|
5
5
|
}
|
|
6
6
|
/** Stable occurrence identity inside a persisted series. */
|
|
7
7
|
export declare function occurrenceIdForSeries(seriesId: LessonSeriesId, academicWeek: number, ordinal?: number): LessonId;
|
|
8
|
+
/** Convert persisted v5 UUID IDs without changing their identity. */
|
|
9
|
+
export declare function migrateLegacyLessonId(id: string): string;
|
package/dist/tt/domain/ids.js
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
|
-
|
|
1
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
2
|
+
const ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
3
|
+
function encodeId(bytes) {
|
|
4
|
+
let encoded = "";
|
|
5
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
6
|
+
const block = (bytes[index] << 16) | (bytes[index + 1] << 8) | bytes[index + 2];
|
|
7
|
+
encoded += ID_ALPHABET[(block >>> 18) & 63];
|
|
8
|
+
encoded += ID_ALPHABET[(block >>> 12) & 63];
|
|
9
|
+
encoded += ID_ALPHABET[(block >>> 6) & 63];
|
|
10
|
+
encoded += ID_ALPHABET[block & 63];
|
|
11
|
+
}
|
|
12
|
+
return encoded;
|
|
13
|
+
}
|
|
14
|
+
function encodeLegacyUuid(uuid) {
|
|
15
|
+
const hex = uuid.replaceAll("-", "").slice(0, 18);
|
|
16
|
+
const bytes = new Uint8Array(hex.match(/.{2}/gu).map((pair) => Number.parseInt(pair, 16)));
|
|
17
|
+
return encodeId(bytes);
|
|
18
|
+
}
|
|
19
|
+
function randomId() {
|
|
2
20
|
const cryptoApi = globalThis.crypto;
|
|
3
|
-
if (cryptoApi?.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const time = Date.now().toString(36);
|
|
7
|
-
return `${prefix}_${time}_${random}`;
|
|
21
|
+
if (!cryptoApi?.getRandomValues)
|
|
22
|
+
throw new Error("Secure random IDs require Web Crypto");
|
|
23
|
+
return encodeId(cryptoApi.getRandomValues(new Uint8Array(9)));
|
|
8
24
|
}
|
|
9
25
|
export class RandomLessonIdGenerator {
|
|
10
26
|
seriesId() {
|
|
11
|
-
return randomId(
|
|
27
|
+
return randomId();
|
|
12
28
|
}
|
|
13
29
|
lessonId() {
|
|
14
|
-
return randomId(
|
|
30
|
+
return randomId();
|
|
15
31
|
}
|
|
16
32
|
}
|
|
17
33
|
/** Stable occurrence identity inside a persisted series. */
|
|
18
34
|
export function occurrenceIdForSeries(seriesId, academicWeek, ordinal = 0) {
|
|
19
|
-
return
|
|
35
|
+
return `${seriesId}.${academicWeek}.${ordinal}`;
|
|
36
|
+
}
|
|
37
|
+
/** Convert persisted v5 UUID IDs without changing their identity. */
|
|
38
|
+
export function migrateLegacyLessonId(id) {
|
|
39
|
+
const occurrence = /^les_ser_([0-9a-f-]{36})_(\d+)_(\d+)$/iu.exec(id);
|
|
40
|
+
if (occurrence && UUID_PATTERN.test(occurrence[1])) {
|
|
41
|
+
return occurrenceIdForSeries(encodeLegacyUuid(occurrence[1]), Number(occurrence[2]), Number(occurrence[3]));
|
|
42
|
+
}
|
|
43
|
+
const series = /^ser_([0-9a-f-]{36})$/iu.exec(id);
|
|
44
|
+
if (series && UUID_PATTERN.test(series[1]))
|
|
45
|
+
return encodeLegacyUuid(series[1]);
|
|
46
|
+
const direct = /^les_([0-9a-f-]{36})$/iu.exec(id);
|
|
47
|
+
if (direct && UUID_PATTERN.test(direct[1]))
|
|
48
|
+
return encodeLegacyUuid(direct[1]);
|
|
49
|
+
return id;
|
|
20
50
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export { RandomLessonIdGenerator, occurrenceIdForSeries } from "./ids.js";
|
|
1
|
+
export { RandomLessonIdGenerator, occurrenceIdForSeries, migrateLegacyLessonId } from "./ids.js";
|
|
2
|
+
export { ScheduleSnapshot, createScheduleSnapshot } from "./schedule-snapshot.js";
|
|
3
|
+
export type { SerializedScheduleSnapshot } from "./schedule-snapshot.js";
|
|
2
4
|
export { TimetableDirectory } from "./directory.js";
|
|
3
5
|
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
|
4
6
|
export { MemoryTimetableRepositoryAdapter, TimetableRepository, } from "./repository.js";
|
|
5
7
|
export { Schedule } from "./schedule.js";
|
|
6
|
-
export { MaterializedSchedule, materializeSchedule, materializeScheduleSnapshot, } from "./materialized-schedule.js";
|
|
7
8
|
export type { ScheduleQueryOptions, ScheduleOptions, ScheduleWeekdayOptions, } from "./schedule.js";
|
|
8
|
-
export type { MaterializeScheduleOptions, MaterializedScheduleSnapshot, SerializedLessonOccurrence, SerializedLessonSourceRef, } from "./materialized-schedule.js";
|
|
9
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, TimetableRepositoryPatch, TimetableRepositorySnapshot, } from "./types.js";
|
package/dist/tt/domain/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { RandomLessonIdGenerator, occurrenceIdForSeries } from "./ids.js";
|
|
1
|
+
export { RandomLessonIdGenerator, occurrenceIdForSeries, migrateLegacyLessonId } from "./ids.js";
|
|
2
|
+
export { ScheduleSnapshot, createScheduleSnapshot } from "./schedule-snapshot.js";
|
|
2
3
|
export { TimetableDirectory } from "./directory.js";
|
|
3
4
|
export { entityKey, mergeEntityRefs, mergeGroups, mergeRooms, mergeTeachers, normalizeScheduleText, } from "./normalize.js";
|
|
4
5
|
export { MemoryTimetableRepositoryAdapter, TimetableRepository, } from "./repository.js";
|
|
5
6
|
export { Schedule } from "./schedule.js";
|
|
6
|
-
export { MaterializedSchedule, materializeSchedule, materializeScheduleSnapshot, } from "./materialized-schedule.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AcademicPeriod, LocalDate } from "../../common/types.js";
|
|
2
|
+
import { type Holiday } from "../utils/index.js";
|
|
3
|
+
import { Schedule, type ScheduleQueryOptions } from "./schedule.js";
|
|
4
|
+
import type { LessonOccurrence, LessonSeries, LessonSourceRef, ScheduleOwner } from "./types.js";
|
|
5
|
+
type SerializedSource = Omit<LessonSourceRef, "observedAt"> & {
|
|
6
|
+
observedAt: string;
|
|
7
|
+
};
|
|
8
|
+
type SerializedSeries = Omit<LessonSeries, "sources"> & {
|
|
9
|
+
sources: SerializedSource[];
|
|
10
|
+
};
|
|
11
|
+
type SerializedOccurrence = Omit<LessonOccurrence, "sources"> & {
|
|
12
|
+
sources: SerializedSource[];
|
|
13
|
+
};
|
|
14
|
+
export interface SerializedScheduleSnapshot {
|
|
15
|
+
schemaVersion: 2;
|
|
16
|
+
repositoryRevision: number;
|
|
17
|
+
owner: ScheduleOwner;
|
|
18
|
+
academicYearStartYear: number;
|
|
19
|
+
period: AcademicPeriod;
|
|
20
|
+
start: LocalDate;
|
|
21
|
+
end: LocalDate;
|
|
22
|
+
series: SerializedSeries[];
|
|
23
|
+
direct: SerializedOccurrence[];
|
|
24
|
+
holidays?: Holiday[];
|
|
25
|
+
holidayTransfers?: {
|
|
26
|
+
dayOff: string;
|
|
27
|
+
workDay: string | null;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/** Browser-ready schedule backed by recurring rules and dated exceptions. */
|
|
31
|
+
export declare class ScheduleSnapshot extends Schedule {
|
|
32
|
+
private readonly snapshot;
|
|
33
|
+
private readonly snapshotSeries;
|
|
34
|
+
private readonly snapshotDirect;
|
|
35
|
+
private readonly dateSetCache;
|
|
36
|
+
constructor(snapshot: SerializedScheduleSnapshot);
|
|
37
|
+
get revision(): number;
|
|
38
|
+
protected queryData(): {
|
|
39
|
+
series: LessonSeries[];
|
|
40
|
+
direct: LessonOccurrence[];
|
|
41
|
+
};
|
|
42
|
+
export(): SerializedScheduleSnapshot;
|
|
43
|
+
on(date: Date, options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
44
|
+
dateKeys(options?: ScheduleQueryOptions): ReadonlySet<LocalDate>;
|
|
45
|
+
}
|
|
46
|
+
export declare function createScheduleSnapshot(schedule: Schedule, options?: {
|
|
47
|
+
start?: Date;
|
|
48
|
+
end?: Date;
|
|
49
|
+
includeSources?: boolean;
|
|
50
|
+
}): SerializedScheduleSnapshot;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { formatLocalDate, parseLocalDate, RUSSIAN_HOLIDAYS } from "../utils/index.js";
|
|
2
|
+
import { Schedule } from "./schedule.js";
|
|
3
|
+
function serializeSources(sources, include) {
|
|
4
|
+
return include
|
|
5
|
+
? sources.map((source) => ({ ...structuredClone(source), observedAt: source.observedAt.toISOString() }))
|
|
6
|
+
: [];
|
|
7
|
+
}
|
|
8
|
+
function deserializeSources(sources) {
|
|
9
|
+
return sources.map((source) => ({ ...structuredClone(source), observedAt: new Date(source.observedAt) }));
|
|
10
|
+
}
|
|
11
|
+
function usesDefaultHolidays(holidays) {
|
|
12
|
+
return holidays.length === RUSSIAN_HOLIDAYS.length && holidays.every((holiday, index) => {
|
|
13
|
+
const standard = RUSSIAN_HOLIDAYS[index];
|
|
14
|
+
return holiday.month === standard.month && holiday.day === standard.day && holiday.name === standard.name;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** Browser-ready schedule backed by recurring rules and dated exceptions. */
|
|
18
|
+
export class ScheduleSnapshot extends Schedule {
|
|
19
|
+
snapshot;
|
|
20
|
+
snapshotSeries;
|
|
21
|
+
snapshotDirect;
|
|
22
|
+
dateSetCache = new Map();
|
|
23
|
+
constructor(snapshot) {
|
|
24
|
+
if (snapshot.schemaVersion !== 2) {
|
|
25
|
+
throw new Error(`Unsupported schedule snapshot schema: ${snapshot.schemaVersion}`);
|
|
26
|
+
}
|
|
27
|
+
const series = snapshot.series.map((value) => ({
|
|
28
|
+
...structuredClone(value), sources: deserializeSources(value.sources),
|
|
29
|
+
}));
|
|
30
|
+
const direct = snapshot.direct.map((value) => ({
|
|
31
|
+
...structuredClone(value), sources: deserializeSources(value.sources),
|
|
32
|
+
}));
|
|
33
|
+
const options = {
|
|
34
|
+
period: snapshot.period,
|
|
35
|
+
holidays: snapshot.holidays,
|
|
36
|
+
holidayTransfers: snapshot.holidayTransfers?.map((value) => ({
|
|
37
|
+
dayOff: new Date(value.dayOff),
|
|
38
|
+
workDay: value.workDay ? new Date(value.workDay) : null,
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
super(undefined, snapshot.owner, snapshot.academicYearStartYear, options);
|
|
42
|
+
this.snapshotSeries = series;
|
|
43
|
+
this.snapshotDirect = direct;
|
|
44
|
+
this.snapshot = structuredClone(snapshot);
|
|
45
|
+
}
|
|
46
|
+
get revision() { return this.snapshot.repositoryRevision; }
|
|
47
|
+
queryData() {
|
|
48
|
+
return { series: this.snapshotSeries, direct: this.snapshotDirect };
|
|
49
|
+
}
|
|
50
|
+
export() { return structuredClone(this.snapshot); }
|
|
51
|
+
on(date, options) {
|
|
52
|
+
const key = formatLocalDate(date);
|
|
53
|
+
return key < this.snapshot.start || key > this.snapshot.end ? [] : super.on(date, options);
|
|
54
|
+
}
|
|
55
|
+
dateKeys(options) {
|
|
56
|
+
const subgroup = options?.subgroup;
|
|
57
|
+
const cached = this.dateSetCache.get(subgroup);
|
|
58
|
+
if (cached)
|
|
59
|
+
return new Set(cached);
|
|
60
|
+
const dates = new Set();
|
|
61
|
+
const end = parseLocalDate(this.snapshot.end);
|
|
62
|
+
for (const date = parseLocalDate(this.snapshot.start); date <= end; date.setDate(date.getDate() + 1)) {
|
|
63
|
+
if (this.on(date, options).length)
|
|
64
|
+
dates.add(formatLocalDate(date));
|
|
65
|
+
}
|
|
66
|
+
this.dateSetCache.set(subgroup, dates);
|
|
67
|
+
return new Set(dates);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function createScheduleSnapshot(schedule, options) {
|
|
71
|
+
const start = options?.start == null ? new Date(schedule.academicYearStartYear, 8, 1) : new Date(options.start);
|
|
72
|
+
const end = options?.end == null ? new Date(schedule.academicYearStartYear + 1, 7, 31) : new Date(options.end);
|
|
73
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime())) {
|
|
74
|
+
throw new RangeError("Schedule snapshot range contains an invalid date");
|
|
75
|
+
}
|
|
76
|
+
if (start > end)
|
|
77
|
+
throw new RangeError("Schedule snapshot start must not exceed end");
|
|
78
|
+
const includeSources = options?.includeSources !== false;
|
|
79
|
+
const series = schedule.series().map((value) => ({
|
|
80
|
+
...value, sources: serializeSources(value.sources, includeSources),
|
|
81
|
+
}));
|
|
82
|
+
const direct = schedule.directOccurrences().map((value) => ({
|
|
83
|
+
...value, sources: serializeSources(value.sources, includeSources),
|
|
84
|
+
}));
|
|
85
|
+
return {
|
|
86
|
+
schemaVersion: 2,
|
|
87
|
+
repositoryRevision: schedule.revision,
|
|
88
|
+
owner: structuredClone(schedule.owner),
|
|
89
|
+
academicYearStartYear: schedule.academicYearStartYear,
|
|
90
|
+
period: schedule.period,
|
|
91
|
+
start: formatLocalDate(start),
|
|
92
|
+
end: formatLocalDate(end),
|
|
93
|
+
series,
|
|
94
|
+
direct,
|
|
95
|
+
...(!usesDefaultHolidays(schedule.holidays) ? { holidays: structuredClone(schedule.holidays) } : {}),
|
|
96
|
+
...(schedule.holidayTransfers.length ? {
|
|
97
|
+
holidayTransfers: schedule.holidayTransfers.map((value) => ({
|
|
98
|
+
dayOff: value.dayOff.toISOString(),
|
|
99
|
+
workDay: value.workDay?.toISOString() ?? null,
|
|
100
|
+
})),
|
|
101
|
+
} : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
5
4
|
import type { LessonOccurrence, LessonSeries, ScheduleOwner } from "./types.js";
|
|
6
5
|
export interface ScheduleQueryOptions {
|
|
7
6
|
subgroup?: number;
|
|
@@ -15,7 +14,7 @@ export interface ScheduleOptions {
|
|
|
15
14
|
holidayTransfers?: HolidayTransfer[];
|
|
16
15
|
}
|
|
17
16
|
export declare class Schedule {
|
|
18
|
-
readonly repository
|
|
17
|
+
readonly repository?: TimetableRepository;
|
|
19
18
|
readonly owner: ScheduleOwner;
|
|
20
19
|
readonly academicYearStartYear: number;
|
|
21
20
|
readonly period: AcademicPeriod;
|
|
@@ -24,13 +23,15 @@ export declare class Schedule {
|
|
|
24
23
|
private cachedRevision;
|
|
25
24
|
private cachedSeries;
|
|
26
25
|
private cachedDirectOccurrences;
|
|
27
|
-
constructor(repository: TimetableRepository, owner: ScheduleOwner, academicYearStartYear: number, options?: ScheduleOptions);
|
|
26
|
+
constructor(repository: TimetableRepository | undefined, owner: ScheduleOwner, academicYearStartYear: number, options?: ScheduleOptions);
|
|
28
27
|
get revision(): number;
|
|
29
|
-
|
|
28
|
+
protected queryData(): {
|
|
29
|
+
series: LessonSeries[];
|
|
30
|
+
direct: LessonOccurrence[];
|
|
31
|
+
};
|
|
30
32
|
series(): LessonSeries[];
|
|
33
|
+
directOccurrences(): LessonOccurrence[];
|
|
31
34
|
on(date: Date, options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
32
|
-
materialize(options?: MaterializeScheduleOptions): MaterializedSchedule;
|
|
33
|
-
materializeSnapshot(options?: MaterializeScheduleOptions): MaterializedScheduleSnapshot;
|
|
34
35
|
week(week?: number, options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
35
36
|
weekday(weekday: number, options?: ScheduleWeekdayOptions): LessonOccurrence[];
|
|
36
37
|
today(options?: ScheduleQueryOptions): LessonOccurrence[];
|
|
@@ -2,7 +2,6 @@ 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";
|
|
6
5
|
function dateAt(date, time) {
|
|
7
6
|
const value = new Date(date);
|
|
8
7
|
value.setHours(time.hours, time.minutes, 0, 0);
|
|
@@ -92,19 +91,22 @@ export class Schedule {
|
|
|
92
91
|
}));
|
|
93
92
|
}
|
|
94
93
|
get revision() {
|
|
95
|
-
return this.repository
|
|
94
|
+
return this.repository?.revision ?? 0;
|
|
96
95
|
}
|
|
97
96
|
queryData() {
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
const repository = this.repository;
|
|
98
|
+
if (!repository)
|
|
99
|
+
throw new Error("Schedule has no live repository");
|
|
100
|
+
if (this.cachedRevision !== repository.revision) {
|
|
101
|
+
this.cachedSeries = repository.getSeries({
|
|
100
102
|
owner: this.owner,
|
|
101
103
|
academicYearStartYear: this.academicYearStartYear,
|
|
102
104
|
});
|
|
103
|
-
this.cachedDirectOccurrences =
|
|
105
|
+
this.cachedDirectOccurrences = repository.getDirectOccurrences({
|
|
104
106
|
owner: this.owner,
|
|
105
107
|
academicYearStartYear: this.academicYearStartYear,
|
|
106
108
|
});
|
|
107
|
-
this.cachedRevision =
|
|
109
|
+
this.cachedRevision = repository.revision;
|
|
108
110
|
}
|
|
109
111
|
return {
|
|
110
112
|
series: this.cachedSeries,
|
|
@@ -114,6 +116,9 @@ export class Schedule {
|
|
|
114
116
|
series() {
|
|
115
117
|
return structuredClone(this.queryData().series);
|
|
116
118
|
}
|
|
119
|
+
directOccurrences() {
|
|
120
|
+
return structuredClone(this.queryData().direct);
|
|
121
|
+
}
|
|
117
122
|
on(date, options) {
|
|
118
123
|
if (!this.isInAcademicYear(date))
|
|
119
124
|
return [];
|
|
@@ -198,12 +203,6 @@ export class Schedule {
|
|
|
198
203
|
}
|
|
199
204
|
return occurrences.sort(sortOccurrences);
|
|
200
205
|
}
|
|
201
|
-
materialize(options) {
|
|
202
|
-
return materializeSchedule(this, options);
|
|
203
|
-
}
|
|
204
|
-
materializeSnapshot(options) {
|
|
205
|
-
return materializeScheduleSnapshot(this, options);
|
|
206
|
-
}
|
|
207
206
|
week(week, options) {
|
|
208
207
|
const period = this.period === AcademicPeriod.WinterSession ||
|
|
209
208
|
this.period === AcademicPeriod.SummerSession
|
package/docs/v5-migration.md
CHANGED
|
@@ -292,25 +292,31 @@ const snapshot = await client.exportRepository();
|
|
|
292
292
|
снимка. Используйте его для экспорта, резервной копии или миграции, не на пути
|
|
293
293
|
каждого запроса.
|
|
294
294
|
|
|
295
|
-
Для API, SSR и офлайн-кеша
|
|
295
|
+
Для API, SSR и офлайн-кеша передавайте компактное расписание владельца:
|
|
296
296
|
|
|
297
297
|
```ts
|
|
298
|
-
|
|
299
|
-
|
|
298
|
+
import { createScheduleSnapshot } from "chuvsu-js";
|
|
299
|
+
|
|
300
|
+
// Сервер: повторяющиеся серии + отдельные пары с датами.
|
|
301
|
+
const payload = createScheduleSnapshot(schedule, { includeSources: false });
|
|
300
302
|
return Response.json(payload);
|
|
301
303
|
|
|
302
|
-
// Браузер:
|
|
303
|
-
import {
|
|
304
|
+
// Браузер: даты строятся из правил расписания.
|
|
305
|
+
import { ScheduleSnapshot } from "chuvsu-js/browser";
|
|
304
306
|
|
|
305
|
-
const schedule = new
|
|
306
|
-
schedule.on(date);
|
|
307
|
+
const schedule = new ScheduleSnapshot(await response.json());
|
|
308
|
+
schedule.on(date);
|
|
307
309
|
schedule.dateKeys({ subgroup: 2 }); // даты для календаря
|
|
308
310
|
```
|
|
309
311
|
|
|
310
|
-
`
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
`SerializedScheduleSnapshot` содержит серии и отдельные пары, но не канонический
|
|
313
|
+
репозиторий. `repositoryRevision` позволяет серверному кешу инвалидировать снимок
|
|
314
|
+
после дополнения пары из другого расписания. ` ScheduleSnapshot` использует ту же
|
|
315
|
+
логику дат, переносов, праздников и подгрупп, что и `Schedule`.
|
|
316
|
+
|
|
317
|
+
Формат `SerializedScheduleSnapshot` имеет `schemaVersion: 2`.
|
|
318
|
+
Новые UUID идентификаторы серий и пар кодируются короче; для перевода сохранённых
|
|
319
|
+
идентификаторов v5 используйте `migrateLegacyLessonId`.
|
|
314
320
|
|
|
315
321
|
Не передавайте экспорт `exportCache()` из v4 в `importCache()` v5: изменились
|
|
316
322
|
ключи, категории и форма закешированных страниц. Старый TTL-кеш и старые blob-
|
package/package.json
CHANGED
|
@@ -1,40 +0,0 @@
|
|
|
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;
|
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
}
|