chuvsu-js 4.2.0 → 5.0.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/README.md +165 -193
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/common/cache.js +30 -9
- package/dist/common/http.d.ts +6 -0
- package/dist/common/http.js +8 -1
- package/dist/common/parse.d.ts +1 -1
- package/dist/common/parse.js +2 -2
- package/dist/common/types.d.ts +8 -2
- package/dist/common/types.js +12 -0
- package/dist/core.d.ts +10 -0
- package/dist/core.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -4
- package/dist/lk/client.d.ts +8 -6
- package/dist/lk/client.js +51 -23
- package/dist/lk/types.d.ts +7 -7
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/parsers.d.ts +3 -0
- package/dist/parsers.js +2 -0
- package/dist/tt/client.d.ts +55 -63
- package/dist/tt/client.js +477 -226
- package/dist/tt/domain/directory.d.ts +14 -0
- package/dist/tt/domain/directory.js +116 -0
- package/dist/tt/domain/ids.d.ts +7 -0
- package/dist/tt/domain/ids.js +20 -0
- package/dist/tt/domain/index.d.ts +7 -0
- package/dist/tt/domain/index.js +5 -0
- package/dist/tt/domain/normalize.d.ts +7 -0
- package/dist/tt/domain/normalize.js +65 -0
- package/dist/tt/domain/repository.d.ts +51 -0
- package/dist/tt/domain/repository.js +805 -0
- package/dist/tt/domain/schedule.d.ts +37 -0
- package/dist/tt/domain/schedule.js +292 -0
- package/dist/tt/domain/types.d.ts +175 -0
- package/dist/tt/domain/types.js +1 -0
- package/dist/tt/observations.d.ts +11 -0
- package/dist/tt/observations.js +131 -0
- package/dist/tt/parse/audience.d.ts +3 -3
- package/dist/tt/parse/audience.js +47 -15
- package/dist/tt/parse/entry-parts.d.ts +1 -0
- package/dist/tt/parse/entry-parts.js +5 -2
- package/dist/tt/parse/full-schedule.d.ts +9 -4
- package/dist/tt/parse/full-schedule.js +32 -41
- package/dist/tt/parse/groups.d.ts +1 -1
- package/dist/tt/parse/groups.js +1 -1
- package/dist/tt/parse/index.d.ts +4 -4
- package/dist/tt/parse/index.js +4 -4
- package/dist/tt/parse/lists.d.ts +6 -5
- package/dist/tt/parse/lists.js +7 -2
- package/dist/tt/parse/overlays.d.ts +5 -4
- package/dist/tt/parse/overlays.js +13 -14
- package/dist/tt/parse/teacher.d.ts +2 -3
- package/dist/tt/parse/teacher.js +14 -56
- package/dist/tt/parse/webinars.js +3 -4
- package/dist/tt/types.d.ts +54 -69
- package/dist/tt/utils/date.d.ts +4 -0
- package/dist/tt/utils/date.js +30 -1
- package/dist/tt/utils/index.d.ts +2 -3
- package/dist/tt/utils/index.js +2 -3
- package/dist/tt/utils/period.d.ts +4 -4
- package/dist/tt/utils/period.js +9 -8
- package/dist/tt/utils/semester.d.ts +4 -4
- package/dist/tt/utils/semester.js +25 -5
- package/dist/tt/utils/time-slots.d.ts +3 -5
- package/dist/tt/utils/time-slots.js +19 -34
- package/dist/tt/webinars.d.ts +7 -0
- package/dist/tt/webinars.js +38 -0
- package/docs/fixture-review.md +56 -0
- package/docs/testing.md +23 -0
- package/docs/v5-architecture.md +144 -0
- package/docs/v5-migration.md +451 -0
- package/package.json +17 -7
- package/dist/shared.d.ts +0 -9
- package/dist/shared.js +0 -7
- package/dist/tt/schedule.d.ts +0 -55
- package/dist/tt/schedule.js +0 -233
- package/dist/tt/utils/lessons.d.ts +0 -16
- package/dist/tt/utils/lessons.js +0 -173
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Архитектура расписания chuvsu-js v5
|
|
2
|
+
|
|
3
|
+
## Цели
|
|
4
|
+
|
|
5
|
+
- Одна реальная пара хранится один раз, даже если разные страницы показывают
|
|
6
|
+
разные части информации о ней.
|
|
7
|
+
- Серии и конкретные занятия получают устойчивые случайные ID, не зависящие от
|
|
8
|
+
изменяемых даты, аудитории и времени.
|
|
9
|
+
- Семестровое расписание и сессия становятся форматами входных наблюдений одной
|
|
10
|
+
модели запросов.
|
|
11
|
+
- TTL-кеш страниц не управляет временем жизни канонических ID.
|
|
12
|
+
- Репозиторий, календарь и запросы работают в браузере без Node-сети и DOM.
|
|
13
|
+
|
|
14
|
+
Версия 5 намеренно несовместима с v4; старые формы вызовов не сохраняются.
|
|
15
|
+
|
|
16
|
+
## Границы пакетов
|
|
17
|
+
|
|
18
|
+
- `chuvsu-js` и `chuvsu-js/node` — Node-клиенты, парсеры и ядро;
|
|
19
|
+
- `chuvsu-js/browser` — репозиторий, `Schedule`, календарные функции, доменные
|
|
20
|
+
типы и импорт/экспорт снимков;
|
|
21
|
+
- `chuvsu-js/parsers` — серверные HTML-парсеры без авторизованных клиентов.
|
|
22
|
+
|
|
23
|
+
Браузерная точка входа не импортирует `undici`, `linkedom`, сертификаты или
|
|
24
|
+
`Buffer`.
|
|
25
|
+
|
|
26
|
+
## Наблюдение и каноническая сущность
|
|
27
|
+
|
|
28
|
+
Строка страницы — не готовая пара, а `ScheduleObservation`. Владелец страницы
|
|
29
|
+
является достоверным неявным фактом: страница группы доказывает участие группы,
|
|
30
|
+
страница преподавателя — преподавателя, страница аудитории — аудиторию.
|
|
31
|
+
|
|
32
|
+
Отсутствующие поля означают «источник не сообщил». Явное отсутствие передается
|
|
33
|
+
как `null` на границе парсера и превращается в пустой `RelationSet` с
|
|
34
|
+
`completeness: "complete"`.
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
RelationSet<T> = {
|
|
38
|
+
values: T[]
|
|
39
|
+
completeness: "unknown" | "partial" | "complete"
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Это устраняет пустые строки и не позволяет принять нехватку данных за известное
|
|
44
|
+
отсутствие.
|
|
45
|
+
|
|
46
|
+
## Время и дата
|
|
47
|
+
|
|
48
|
+
Номер и фактический интервал независимы:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
slotNumber?: number
|
|
52
|
+
time?: { start: Time, end: Time }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Стандартная сетка звонков — только справочная информация и не переписывает
|
|
56
|
+
утверждение портала. Сессия не получает вычисленный номер пары по ближайшему
|
|
57
|
+
времени.
|
|
58
|
+
|
|
59
|
+
Дата без времени — `LocalDate` (`YYYY-MM-DD`). В модели не используется
|
|
60
|
+
полуночный `Date`, поэтому сериализация через UTC не может сдвинуть учебный день.
|
|
61
|
+
|
|
62
|
+
## Серии и занятия
|
|
63
|
+
|
|
64
|
+
`LessonSeries` описывает повторение в семестре: учебный год, период, день недели,
|
|
65
|
+
необязательный диапазон недель, чередование, независимые номер и время, предмет,
|
|
66
|
+
тип, связи, замены и происхождение данных. Отсутствующий диапазон означает, что
|
|
67
|
+
источник не ограничил недели; фиктивные диапазоны вроде `0..0` запрещены.
|
|
68
|
+
|
|
69
|
+
`LessonOccurrence` описывает конкретное занятие. У него есть `scheduledDate`,
|
|
70
|
+
`nominalDate`, статус переноса, отдельное локальное время и, для семестровой
|
|
71
|
+
пары, `seriesId`. Экзамен, консультация и другая строка сессии сразу становятся
|
|
72
|
+
`LessonOccurrence` с датой из страницы: у них нет `recurrence` и диапазона недель.
|
|
73
|
+
|
|
74
|
+
ID серии создается генератором и хранится в репозитории. ID семестрового
|
|
75
|
+
занятия выводится из ID серии, учебной недели и порядкового номера возникновения;
|
|
76
|
+
дата, время и аудитория в идентичность не входят. Строка сессии получает
|
|
77
|
+
сохраненный `lessonId`.
|
|
78
|
+
|
|
79
|
+
## Согласование наблюдений
|
|
80
|
+
|
|
81
|
+
При загрузке источника репозиторий:
|
|
82
|
+
|
|
83
|
+
1. Нормализует текст и ссылки на сущности.
|
|
84
|
+
2. Сохраняет прежний ID, если наблюдение связано с уже известной строкой этого
|
|
85
|
+
источника.
|
|
86
|
+
3. Для нового источника рассматривает только тот же учебный год, период и вид
|
|
87
|
+
сущности.
|
|
88
|
+
4. Запрещает объединение разных дат конкретных занятий, разных дней недели,
|
|
89
|
+
непересекающихся недель и несовместимого чередования.
|
|
90
|
+
5. Независимо оценивает совпадение номера и времени, а также предмета, типа,
|
|
91
|
+
групп, преподавателей и аудиторий.
|
|
92
|
+
6. Объединяет только единственного кандидата с достаточным доказательством;
|
|
93
|
+
неоднозначность создает новый ID.
|
|
94
|
+
7. Хранит происхождение всех утверждений и объединяет совместимые связи.
|
|
95
|
+
8. Выбирает номер пары и интервал времени независимо по совокупности источников,
|
|
96
|
+
поэтому ни одна «самая полная» строка не перетирает остальные поля целиком.
|
|
97
|
+
|
|
98
|
+
Результат проверяется в прямом и обратном порядке загрузки корпуса. Совпавшее
|
|
99
|
+
время может сохранить идентичность при ошибочном номере пары, но разные дни или
|
|
100
|
+
чередование не могут быть склеены похожими метаданными.
|
|
101
|
+
|
|
102
|
+
## Справочник сущностей
|
|
103
|
+
|
|
104
|
+
Репозиторий содержит справочник групп, преподавателей и аудиторий. Его наполняют
|
|
105
|
+
поиск, списки и владельцы загруженных страниц. Сокращение преподавателя
|
|
106
|
+
разрешается по фамилии и инициалам только при единственном совпадении.
|
|
107
|
+
|
|
108
|
+
Загрузка расписания не делает скрытых сетевых запросов. Допустимы только явно
|
|
109
|
+
вызванные `resolve*` со стратегией поиска или `preloadDirectory`.
|
|
110
|
+
|
|
111
|
+
## Хранилища
|
|
112
|
+
|
|
113
|
+
Есть три независимых слоя:
|
|
114
|
+
|
|
115
|
+
1. TTL-кеш сетевых ответов и метаданных.
|
|
116
|
+
2. Канонический репозиторий ID, наблюдений, связей и ревизии.
|
|
117
|
+
3. Производные представления запросов, инвалидируемые ревизией.
|
|
118
|
+
|
|
119
|
+
Снимок репозитория имеет `schemaVersion: 5`. Адаптер постоянного хранилища
|
|
120
|
+
использует compare-and-set по ревизии.
|
|
121
|
+
|
|
122
|
+
## Запросы
|
|
123
|
+
|
|
124
|
+
`Schedule` — легкое синхронное представление владельца поверх репозитория:
|
|
125
|
+
|
|
126
|
+
- `on(date)`;
|
|
127
|
+
- `week(academicWeek?)`;
|
|
128
|
+
- `weekday(day, options?)`;
|
|
129
|
+
- `today()`, `tomorrow()`, `thisWeek()`;
|
|
130
|
+
- `current()`;
|
|
131
|
+
- `series()`.
|
|
132
|
+
|
|
133
|
+
При раскрытии серии применяются недели, праздники, замены и переносы, после чего
|
|
134
|
+
добавляются прямые занятия сессии.
|
|
135
|
+
|
|
136
|
+
## Проверка
|
|
137
|
+
|
|
138
|
+
Полностраничный корпус содержит 34 страницы групп, 4 преподавателей и 4
|
|
139
|
+
аудиторий. Для каждой страницы есть полный вручную проверенный JSON-эталон;
|
|
140
|
+
всего проверено 1184 занятия. Тест сравнивает весь канонический результат, а не
|
|
141
|
+
отдельные поля. Генератора эталонов в репозитории нет. Журнал ручной проверки
|
|
142
|
+
находится в [`fixture-review.md`](fixture-review.md). Отдельные тесты проверяют
|
|
143
|
+
устойчивость ID, неоднозначность, объединение трех проекций, порядок запросов,
|
|
144
|
+
локальные даты, три состояния связей и границу браузерного пакета.
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# Переход с v4 на v5
|
|
2
|
+
|
|
3
|
+
Версия 5 полностью меняет модель расписания и намеренно не содержит слоя
|
|
4
|
+
совместимости с v4. Основное отличие: в v4 каждый запрос возвращал независимую
|
|
5
|
+
копию HTML-таблицы, а в v5 страницы групп, преподавателей и аудиторий пополняют
|
|
6
|
+
единый канонический репозиторий. Одна реальная пара получает один ID и собирает
|
|
7
|
+
из разных страниц все известные связи.
|
|
8
|
+
|
|
9
|
+
## Установка
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install chuvsu-js@^5
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Рекомендуется Node.js 20+ и ESM. Сетевые клиенты доступны из `chuvsu-js` и
|
|
16
|
+
`chuvsu-js/node`; браузерное ядро — из `chuvsu-js/browser`; HTML-парсеры — из
|
|
17
|
+
`chuvsu-js/parsers`.
|
|
18
|
+
|
|
19
|
+
## Минимальная миграция
|
|
20
|
+
|
|
21
|
+
### v4
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { TtClient } from "chuvsu-js";
|
|
25
|
+
|
|
26
|
+
const client = new TtClient({ cache: 15 * 60_000 });
|
|
27
|
+
await client.loginAsGuest();
|
|
28
|
+
|
|
29
|
+
const [group] = await client.searchGroup({ name: "КТ-41-24" });
|
|
30
|
+
const schedule = await client.getSchedule(group.id);
|
|
31
|
+
|
|
32
|
+
for (const lesson of schedule.forDate(new Date())) {
|
|
33
|
+
console.log(lesson.number, lesson.subject, lesson.room, lesson.teacher);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### v5
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { TimetableClient } from "chuvsu-js";
|
|
41
|
+
|
|
42
|
+
const client = new TimetableClient({ cache: 15 * 60_000 });
|
|
43
|
+
await client.loginAsGuest();
|
|
44
|
+
|
|
45
|
+
const [group] = await client.searchGroups("КТ-41-24");
|
|
46
|
+
const schedule = await client.getGroupSchedule(group.id);
|
|
47
|
+
|
|
48
|
+
for (const lesson of schedule.on(new Date())) {
|
|
49
|
+
console.log(
|
|
50
|
+
lesson.id,
|
|
51
|
+
lesson.slotNumber,
|
|
52
|
+
lesson.subject,
|
|
53
|
+
lesson.rooms.values,
|
|
54
|
+
lesson.teachers.values,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Переименования клиентов и общих типов
|
|
60
|
+
|
|
61
|
+
| v4 | v5 |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `TtClient` | `TimetableClient` |
|
|
64
|
+
| `TtClientOptions` | `TimetableClientOptions` |
|
|
65
|
+
| `LkClient` | `StudentPortalClient` |
|
|
66
|
+
| `LkClientOptions` | `StudentPortalClientOptions` |
|
|
67
|
+
| `LkCacheConfig` | `StudentPortalCacheConfig` |
|
|
68
|
+
| `PersonalData` | `StudentProfile` |
|
|
69
|
+
| `Audience` | `Room` |
|
|
70
|
+
| `AudienceInfo` | `RoomInfo` |
|
|
71
|
+
| `EducationType` | `EducationLevel` |
|
|
72
|
+
| `Period` | `AcademicPeriod` |
|
|
73
|
+
|
|
74
|
+
Опция клиента также переименована:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// v4
|
|
78
|
+
new TtClient({ educationType: EducationType.HigherEducation });
|
|
79
|
+
|
|
80
|
+
// v5
|
|
81
|
+
new TimetableClient({ educationLevel: EducationLevel.HigherEducation });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Методы `TimetableClient`
|
|
85
|
+
|
|
86
|
+
| v4 | v5 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `getSchedule(groupId)` | `getGroupSchedule(groupId)` |
|
|
89
|
+
| `getScheduleForPeriod({ groupId, period })` | `getGroupSchedule(groupId, { periods: [period] })` |
|
|
90
|
+
| `getTeacherScheduleForPeriod({ teacherId, period })` | `getTeacherSchedule(teacherId, { periods: [period] })` |
|
|
91
|
+
| `getAudienceSchedule(id)` | `getRoomSchedule(id)` |
|
|
92
|
+
| `getAudienceScheduleForPeriod({ audienceId, period })` | `getRoomSchedule(audienceId, { periods: [period] })` |
|
|
93
|
+
| `searchGroup({ name })` | `searchGroups(name)` |
|
|
94
|
+
| `searchTeacher({ name })` | `searchTeachers(name)` |
|
|
95
|
+
| `searchAudience({ name })` | `searchRooms(name)` |
|
|
96
|
+
| `getGroupsForFaculty({ facultyId })` | `getFacultyGroups(facultyId)` |
|
|
97
|
+
| `getAudiences()` | `getRooms()` |
|
|
98
|
+
| `findAudienceByName({ name })` | `findRoomByName(name)` |
|
|
99
|
+
| `getAudienceName(id)` | `getRoomName(id)` |
|
|
100
|
+
| `getAudienceInfo(id)` | `getRoomInfo(id)` |
|
|
101
|
+
| `getAudienceImage(id)` | `getRoomImage(id)` |
|
|
102
|
+
| `getAudienceBlockImage(id)` | `getRoomBuildingImage(id)` |
|
|
103
|
+
| `getAudienceFloorplan(id)` | `getRoomFloorPlan(id)` |
|
|
104
|
+
| `getTeacherPhoto(id)` / `getTeacherPhotoLazy(id)` | `getTeacherPhoto(id)` |
|
|
105
|
+
|
|
106
|
+
`getGroupSchedule`, `getTeacherSchedule` и `getRoomSchedule` по умолчанию
|
|
107
|
+
загружают все четыре периода. Для ограничения запросов передайте `periods`:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { AcademicPeriod } from "chuvsu-js";
|
|
111
|
+
|
|
112
|
+
const schedule = await client.getTeacherSchedule(teacherId, {
|
|
113
|
+
periods: [
|
|
114
|
+
AcademicPeriod.FallSemester,
|
|
115
|
+
AcademicPeriod.WinterSession,
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Для кода, который работает с разными владельцами одинаково, добавлен общий
|
|
121
|
+
метод:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const schedule = await client.getSchedule({
|
|
125
|
+
type: "room",
|
|
126
|
+
room: { id: roomId, name: "Г-402" },
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Методы `Schedule`
|
|
131
|
+
|
|
132
|
+
| v4 | v5 |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `forDate(date)` | `on(date)` |
|
|
135
|
+
| `forWeek(week, options)` | `week(week, options)` |
|
|
136
|
+
| `forDay(weekday, options)` | `weekday(weekday, options)` |
|
|
137
|
+
| `currentLesson(options)` | `current(options)` |
|
|
138
|
+
| `today(options)` | `today(options)` |
|
|
139
|
+
| `tomorrow(options)` | `tomorrow(options)` |
|
|
140
|
+
| `thisWeek(options)` | `thisWeek(options)` |
|
|
141
|
+
|
|
142
|
+
`on`, `week`, `weekday`, `today`, `tomorrow` и `thisWeek` теперь всегда
|
|
143
|
+
возвращают `LessonOccurrence[]`. Опция `subgroup` сохранена:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const lessons = schedule.weekday(1, { week: 4, subgroup: 2 });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Новый метод `series()` возвращает повторяющиеся определения
|
|
150
|
+
`LessonSeries[]`. Поля v4 `scheduleMap`, `days`, `periods` и `getDays()` удалены:
|
|
151
|
+
канонический `Schedule` больше не является оберткой над HTML-таблицей.
|
|
152
|
+
|
|
153
|
+
## Миграция `Lesson` на `LessonOccurrence`
|
|
154
|
+
|
|
155
|
+
| v4 `Lesson` | v5 `LessonOccurrence` |
|
|
156
|
+
|---|---|
|
|
157
|
+
| `number` | `slotNumber` |
|
|
158
|
+
| `start.hours`, `start.minutes` | `time?.start.hours`, `time?.start.minutes` |
|
|
159
|
+
| `end.hours`, `end.minutes` | `time?.end.hours`, `time?.end.minutes` |
|
|
160
|
+
| `start.date` | `scheduledDate` (`YYYY-MM-DD`) |
|
|
161
|
+
| `room` | `rooms.values` |
|
|
162
|
+
| `teacher` | `teachers.values` |
|
|
163
|
+
| `groups: string[]` | `groups.values: GroupAttendance[]` |
|
|
164
|
+
| `originalRoom` | `originalRooms` |
|
|
165
|
+
| `originalTeacher` | `originalTeachers` |
|
|
166
|
+
| `transfer` | `status`, `nominalDate`, `scheduledDate`, `movedFrom` |
|
|
167
|
+
| отсутствовало | `id` и необязательный `seriesId` |
|
|
168
|
+
| отсутствовало | `sources` |
|
|
169
|
+
|
|
170
|
+
`slotNumber` и `time` необязательны и независимы. Нельзя использовать
|
|
171
|
+
утверждение `lesson.number === 5` как источник времени или вычислять номер пары
|
|
172
|
+
по ближайшему интервалу.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
for (const lesson of schedule.on(date)) {
|
|
176
|
+
const roomNames = lesson.rooms.values.map((room) => room.name);
|
|
177
|
+
const teacherNames = lesson.teachers.values.map((teacher) => teacher.name);
|
|
178
|
+
const groupNames = lesson.groups.values.map(({ group }) => group.name);
|
|
179
|
+
|
|
180
|
+
if (lesson.time) {
|
|
181
|
+
console.log(lesson.time.start, lesson.time.end);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Поля повторения больше не дублируются в конкретном занятии. Диапазон недель,
|
|
187
|
+
день и чередование находятся в `LessonSeries.recurrence`:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
for (const series of schedule.series()) {
|
|
191
|
+
console.log(
|
|
192
|
+
series.id,
|
|
193
|
+
series.recurrence.weekday,
|
|
194
|
+
series.recurrence.weeks,
|
|
195
|
+
series.recurrence.parity,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Отсутствующий `recurrence.weeks` означает, что источник не сообщил ограничение,
|
|
201
|
+
а не диапазон `0..0`.
|
|
202
|
+
|
|
203
|
+
## Связи и отсутствие данных
|
|
204
|
+
|
|
205
|
+
В v4 пустая строка или пустой массив одновременно могли означать «значения
|
|
206
|
+
нет» и «страница его не показала». В v5 группы, преподаватели и аудитории имеют
|
|
207
|
+
тип `RelationSet<T>`:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
type RelationSet<T> = {
|
|
211
|
+
values: T[];
|
|
212
|
+
completeness: "unknown" | "partial" | "complete";
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- `unknown` — источник ничего не сообщил;
|
|
217
|
+
- `partial` — известна только часть списка;
|
|
218
|
+
- `complete` — список известен полностью, включая явный пустой список.
|
|
219
|
+
|
|
220
|
+
Проверяйте не только `values.length`, если для приложения важно различать
|
|
221
|
+
неизвестность и подтвержденное отсутствие.
|
|
222
|
+
|
|
223
|
+
Ссылки на сущности теперь могут содержать ID:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
lesson.teachers.values // TeacherRef[]: { id?, name, position?, degree? }
|
|
227
|
+
lesson.rooms.values // RoomRef[]: { id?, name, building? }
|
|
228
|
+
lesson.groups.values // GroupAttendance[]: { group: GroupRef, subgroup? }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Даты, сессия и переносы
|
|
232
|
+
|
|
233
|
+
Календарные даты внутри модели представлены `LocalDate` — строкой
|
|
234
|
+
`YYYY-MM-DD`. Это предотвращает сдвиг учебного дня при сериализации в UTC.
|
|
235
|
+
Методы запросов по-прежнему принимают локальный `Date`.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import { formatLocalDate, parseLocalDate } from "chuvsu-js";
|
|
239
|
+
|
|
240
|
+
const key = formatLocalDate(new Date(2026, 8, 3)); // "2026-09-03"
|
|
241
|
+
const date = parseLocalDate(key); // локальный Date
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Экзамен, консультация и другая строка сессии сразу являются
|
|
245
|
+
`LessonOccurrence` с датой из портала. У них нет фиктивных недель и обычно нет
|
|
246
|
+
`seriesId`.
|
|
247
|
+
|
|
248
|
+
Для перенесенного занятия:
|
|
249
|
+
|
|
250
|
+
- `nominalDate` — исходная дата;
|
|
251
|
+
- `scheduledDate` — фактическая дата;
|
|
252
|
+
- `status === "moved"`;
|
|
253
|
+
- `movedFrom` содержит исходную дату и номер пары.
|
|
254
|
+
|
|
255
|
+
## ID и постоянное хранилище
|
|
256
|
+
|
|
257
|
+
В v4 устойчивых ID занятий не было. Поэтому старый кеш нельзя преобразовать в
|
|
258
|
+
ID v5. После первого получения данных v5 ID сохраняются в
|
|
259
|
+
`TimetableRepository` и не зависят от даты, аудитории или времени.
|
|
260
|
+
|
|
261
|
+
TTL-кеш сетевых ответов и репозиторий идентичности — разные хранилища:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const client = new TimetableClient({
|
|
265
|
+
cache: 15 * 60_000,
|
|
266
|
+
cacheAdapter,
|
|
267
|
+
repositoryAdapter,
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Для сохранения ID между процессами реализуйте `TimetableRepositoryAdapter` или
|
|
272
|
+
используйте `MemoryTimetableRepositoryAdapter` в рамках одного процесса.
|
|
273
|
+
Снимок можно получить явно:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const snapshot = await client.exportRepository();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Не передавайте экспорт `exportCache()` из v4 в `importCache()` v5: изменились
|
|
280
|
+
ключи, категории и форма закешированных страниц. Старый TTL-кеш и старые blob-
|
|
281
|
+
ключи следует удалить. Снимок репозитория v5 имеет `schemaVersion: 5`.
|
|
282
|
+
|
|
283
|
+
## Дополнение данных и справочник сущностей
|
|
284
|
+
|
|
285
|
+
Полученный `Schedule` является живым представлением репозитория. Последующие
|
|
286
|
+
запросы могут дополнить ту же пару, не меняя ее ID:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
const schedule = await client.getGroupSchedule(groupId);
|
|
290
|
+
const before = schedule.on(date);
|
|
291
|
+
|
|
292
|
+
await client.getTeacherSchedule(teacherId);
|
|
293
|
+
await client.getRoomSchedule(roomId);
|
|
294
|
+
|
|
295
|
+
const after = schedule.on(date);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Загрузка расписания не запускает каскадный поиск групп, преподавателей или
|
|
299
|
+
аудиторий. Справочник можно наполнить явно:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
await client.preloadDirectory({
|
|
303
|
+
teachers: true,
|
|
304
|
+
rooms: true,
|
|
305
|
+
facultyIds: [19],
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const teacher = await client.resolveTeacher("Иванов И. И.");
|
|
309
|
+
const searched = await client.resolveTeacher("Иванов И. И.", {
|
|
310
|
+
strategy: "search",
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Стратегия по умолчанию — `cache-only`. ID сокращенному имени присваивается
|
|
315
|
+
только при единственном совпадении.
|
|
316
|
+
|
|
317
|
+
## HTML-парсеры
|
|
318
|
+
|
|
319
|
+
Если приложение использовало табличные типы v4 напрямую, миграция выполняется
|
|
320
|
+
отдельно от канонического `Schedule`:
|
|
321
|
+
|
|
322
|
+
| v4 | v5 (`chuvsu-js/parsers`) |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `FullScheduleDay` | `ParsedScheduleDay` |
|
|
325
|
+
| `FullScheduleSlot` | `ParsedScheduleBlock` |
|
|
326
|
+
| `ScheduleEntry` | `ParsedLesson` |
|
|
327
|
+
| `day.slots` | `day.blocks` |
|
|
328
|
+
| `slot.number` | `block.slotNumber` |
|
|
329
|
+
| `slot.timeStart`, `slot.timeEnd` | `block.time?.start`, `block.time?.end` |
|
|
330
|
+
| `slot.entries` | `block.lessons` |
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { parseGroupSchedule } from "chuvsu-js/parsers";
|
|
334
|
+
|
|
335
|
+
const days = parseGroupSchedule(html);
|
|
336
|
+
for (const day of days) {
|
|
337
|
+
for (const block of day.blocks) {
|
|
338
|
+
console.log(block.slotNumber, block.time, block.lessons);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`ParsedLesson.room`, `teacher` и `groups` могут быть `undefined` (источник не
|
|
344
|
+
сообщил) или `null` (источник явно сообщил отсутствие). Для загрузки разобранной
|
|
345
|
+
страницы в доменную модель используйте `createScheduleSourceSnapshot`, затем
|
|
346
|
+
`TimetableRepository.ingest`.
|
|
347
|
+
|
|
348
|
+
## Вебинары
|
|
349
|
+
|
|
350
|
+
| v4 | v5 |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `attachWebinarsToLessons` | `attachWebinars` |
|
|
353
|
+
| `matchWebinarToLesson` | `findWebinar` |
|
|
354
|
+
| `webinar.date` | `webinar.scheduledDate` |
|
|
355
|
+
| `webinar.timeStart`, `webinar.timeEnd` | `webinar.time.start`, `webinar.time.end` |
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { attachWebinars } from "chuvsu-js";
|
|
359
|
+
|
|
360
|
+
const lessons = schedule.on(date);
|
|
361
|
+
const webinars = await client.getWebinars({ date });
|
|
362
|
+
const withWebinars = attachWebinars(lessons, webinars);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`attachWebinars` возвращает новые объекты `LessonWithWebinar` и не изменяет
|
|
366
|
+
занятия в репозитории.
|
|
367
|
+
|
|
368
|
+
## Сетка звонков
|
|
369
|
+
|
|
370
|
+
| v4 | v5 |
|
|
371
|
+
|---|---|
|
|
372
|
+
| `getTimeSlots()` | `getStandardScheduleBlocks()` |
|
|
373
|
+
| `getLessonNumber(time)` | удален |
|
|
374
|
+
|
|
375
|
+
Стандартная сетка в v5 является только справочником. Она не исправляет данные
|
|
376
|
+
портала и не используется для вычисления `slotNumber` по времени.
|
|
377
|
+
|
|
378
|
+
## Личный кабинет
|
|
379
|
+
|
|
380
|
+
| v4 | v5 |
|
|
381
|
+
|---|---|
|
|
382
|
+
| `new LkClient()` | `new StudentPortalClient()` |
|
|
383
|
+
| `getPersonalData()` | `getProfile()` |
|
|
384
|
+
| `getPhoto()` | `getProfilePhoto()` |
|
|
385
|
+
| `getGroupId()` | `getTimetableGroupId()` |
|
|
386
|
+
|
|
387
|
+
Поля профиля не изменились; изменились имена класса, типа и методов. Ключи
|
|
388
|
+
настроек кеша переименованы с `personalData`, `photo`, `groupId` на `profile`,
|
|
389
|
+
`profilePhoto`, `timetableGroupId`.
|
|
390
|
+
|
|
391
|
+
## Ключи `CacheConfig`
|
|
392
|
+
|
|
393
|
+
В расписании переименованы категории:
|
|
394
|
+
|
|
395
|
+
| v4 | v5 |
|
|
396
|
+
|---|---|
|
|
397
|
+
| `audiences` | `rooms` |
|
|
398
|
+
| `audienceNames` | `roomNames` |
|
|
399
|
+
| `audienceInfo` | `roomInfo` |
|
|
400
|
+
| `audienceImages` | `roomImages` |
|
|
401
|
+
|
|
402
|
+
Остальные категории сохраняют назначение, но содержимое кеша v4 повторно
|
|
403
|
+
использовать нельзя.
|
|
404
|
+
|
|
405
|
+
## Браузер
|
|
406
|
+
|
|
407
|
+
В v4 браузерный `Schedule` строился из `Map<Period, FullScheduleDay[]>`. В v5
|
|
408
|
+
браузер получает снимок канонического репозитория от сервера:
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
// Node/server
|
|
412
|
+
const snapshot = await client.exportRepository();
|
|
413
|
+
|
|
414
|
+
// Browser
|
|
415
|
+
import { Schedule, TimetableRepository } from "chuvsu-js/browser";
|
|
416
|
+
|
|
417
|
+
const repository = new TimetableRepository({ snapshot });
|
|
418
|
+
const schedule = new Schedule(
|
|
419
|
+
repository,
|
|
420
|
+
{ type: "group", group: { id: 8919, name: "КТ-41-24" } },
|
|
421
|
+
2026,
|
|
422
|
+
);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`chuvsu-js/browser` не включает HTML-парсер, `undici`, сертификаты ЧГУ и
|
|
426
|
+
`Buffer`. Авторизацию и загрузку страниц выполняйте на сервере.
|
|
427
|
+
|
|
428
|
+
## Удаленные допущения v4
|
|
429
|
+
|
|
430
|
+
- Нельзя считать пустую строку аудитории подтвержденным отсутствием аудитории.
|
|
431
|
+
- Нельзя считать номер пары индексом массива или выводить время из номера.
|
|
432
|
+
- Нельзя читать недели у экзамена или консультации: это датированные занятия.
|
|
433
|
+
- Нельзя сравнивать пары по `дата + аудитория + время`; используйте `id`.
|
|
434
|
+
- Нельзя ожидать, что страница группы содержит полное имя и ID преподавателя.
|
|
435
|
+
- Нельзя импортировать TTL-кеш v4 как репозиторий v5.
|
|
436
|
+
- Нельзя создавать `Schedule` из старых `FullScheduleDay[]`.
|
|
437
|
+
|
|
438
|
+
## Рекомендуемый порядок перехода
|
|
439
|
+
|
|
440
|
+
1. Переименовать клиенты, enum и методы поиска.
|
|
441
|
+
2. Заменить методы получения расписаний и вызовы `Schedule`.
|
|
442
|
+
3. Перевести UI и бизнес-логику с `Lesson` на `LessonOccurrence`.
|
|
443
|
+
4. Обработать `RelationSet` и необязательные `slotNumber`/`time`.
|
|
444
|
+
5. Перевести сохраняемые даты на `LocalDate`.
|
|
445
|
+
6. Перенести работу с повторением в `schedule.series()`.
|
|
446
|
+
7. Удалить старый кеш; отдельно подключить `repositoryAdapter`.
|
|
447
|
+
8. Если используются парсеры или браузер, перейти на новые точки входа.
|
|
448
|
+
9. Обновить сопоставление вебинаров и методы личного кабинета.
|
|
449
|
+
10. Проверить сессию, переносы, замены, подгруппы и ДОТ на реальных данных.
|
|
450
|
+
|
|
451
|
+
Описание внутренней модели: [`v5-architecture.md`](v5-architecture.md).
|
package/package.json
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chuvsu-js",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"description": "Библиотека для расписания и личного кабинета ЧГУ им. И. Н. Ульянова",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./node": {
|
|
14
|
+
"types": "./dist/node.d.ts",
|
|
15
|
+
"import": "./dist/node.js"
|
|
12
16
|
},
|
|
13
17
|
"./browser": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
18
|
+
"types": "./dist/browser.d.ts",
|
|
19
|
+
"import": "./dist/browser.js"
|
|
20
|
+
},
|
|
21
|
+
"./parsers": {
|
|
22
|
+
"types": "./dist/parsers.d.ts",
|
|
23
|
+
"import": "./dist/parsers.js"
|
|
16
24
|
}
|
|
17
25
|
},
|
|
18
26
|
"files": [
|
|
19
27
|
"dist",
|
|
20
28
|
"README.md",
|
|
29
|
+
"docs",
|
|
21
30
|
"LICENSE"
|
|
22
31
|
],
|
|
23
32
|
"keywords": [
|
|
@@ -44,8 +53,9 @@
|
|
|
44
53
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
45
54
|
"build": "pnpm clean && tsc",
|
|
46
55
|
"test": "pnpm build && node --test",
|
|
56
|
+
"test:coverage": "pnpm build && node --experimental-test-coverage --test-coverage-include='dist/tt/domain/*.js' --test-coverage-include='dist/tt/observations.js' --test-coverage-include='dist/tt/parse/*.js' --test-coverage-include='dist/tt/utils/*.js' --test-coverage-include='dist/tt/webinars.js' --test-coverage-lines=95 --test-coverage-functions=94 --test-coverage-branches=80 --test",
|
|
47
57
|
"test:live:schedules": "pnpm build && node --env-file=.env utils/testSchedules.mjs",
|
|
48
|
-
"fixtures:
|
|
58
|
+
"fixtures:test": "pnpm build && node --test test/schedule-fixtures.test.mjs",
|
|
49
59
|
"fixtures:collect": "pnpm build && node --env-file=.env utils/collectScheduleFixtures.mjs"
|
|
50
60
|
}
|
|
51
61
|
}
|
package/dist/shared.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export { Schedule } from "./tt/schedule.js";
|
|
2
|
-
export { parseGroupsString, parseWebinars } from "./tt/parse/index.js";
|
|
3
|
-
export { attachWebinarsToLessons, getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, matchWebinarToLesson, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
4
|
-
export type { Holiday, HolidayTransfer } from "./tt/utils/index.js";
|
|
5
|
-
export { AuthError, EducationType, ParseError, Period, } from "./common/types.js";
|
|
6
|
-
export type { Teacher, Time, WeekRange } from "./common/types.js";
|
|
7
|
-
export type { BlobAdapter, BlobPutOptions, CacheAdapter, CacheEntry, } from "./common/cache.js";
|
|
8
|
-
export type { Audience, AudienceInfo, CacheConfig, Faculty, FullScheduleDay, FullScheduleSlot, Group, Lesson, LessonTime, LessonTimeSlot, ScheduleEntry, SemesterWeek, SubstituteForInfo, Substitution, TeacherInfo, TransferInfo, TtClientOptions, Webinar, } from "./tt/types.js";
|
|
9
|
-
export type { LkCacheConfig, LkClientOptions, PersonalData } from "./lk/types.js";
|
package/dist/shared.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// Shared exports that work in any environment (Node, browser, Deno).
|
|
2
|
-
// Both ./index.ts and ./browser.ts re-export everything from here and only
|
|
3
|
-
// add their platform-specific extras on top.
|
|
4
|
-
export { Schedule } from "./tt/schedule.js";
|
|
5
|
-
export { parseGroupsString, parseWebinars } from "./tt/parse/index.js";
|
|
6
|
-
export { attachWebinarsToLessons, getAdjacentSemester, getCompensatingWorkDays, getCurrentPeriod, getEffectiveHolidays, getHolidayTransfers, getLessonNumber, getSemesterStart, getSemesterWeeks, getTimeSlots, getWeekNumber, getWeekdayName, isHoliday, isSessionPeriod, matchWebinarToLesson, RUSSIAN_HOLIDAYS, } from "./tt/utils/index.js";
|
|
7
|
-
export { AuthError, ParseError, } from "./common/types.js";
|