bsuir-iis-api 0.7.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,12 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 323feb4: New public APIs, cache, hooks, deps versions update, new JSdoc, etc.
8
+
3
9
  All notable changes to this project are documented in this file.
4
10
 
5
11
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
12
  and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
13
  This changelog is maintained manually and updated in release commits.
8
14
 
9
- ## [Unreleased]
15
+ ## [0.8.0] - 2026-05-10
16
+
17
+ ### Added
18
+
19
+ - Public exports for schedule utilities: `normalizeSchedule` and `filterLessons`.
20
+ - Public re-export of `ScheduleFilterOptions` type from `src/index.ts`.
21
+ - Global cancellation support via `createBsuirClient({ signal })`.
22
+ - `src/modules/index.ts` barrel for module factory imports.
23
+ - In-memory GET cache via `createBsuirClient({ cache: { ttlMs, maxEntries } })`.
24
+ - In-flight GET request deduplication via `dedupeInFlight`.
25
+ - Opt-in runtime response-shape validation via `validateResponses` and `BsuirResponseValidationError`.
26
+ - Request lifecycle hooks via `createBsuirClient({ hooks })`: `onRequest`, `onRetry`, `onResponse`, `onError`.
27
+ - API Extractor configuration (`api-extractor.json`) and npm scripts for local/update and CI checks.
28
+ - `POST` method support in `requestJson` via `options.method` and `options.body`.
29
+
30
+ ### Fixed
31
+
32
+ - `schedule.getGroup/getEmployee` return typing now follows client-level `defaultRaw` when `raw` is omitted.
33
+ - `normalizeSchedule` no longer duplicates day-item flattening work and avoids repeated `auditories` normalization per lesson.
34
+ - Strict date parsing in `parseDdMmYyyy` now rejects non-existent calendar dates (for example `31.02.2026`).
35
+ - `BsuirNetworkError` now relies on standard `Error.cause` instead of duplicate `causeError` field.
36
+ - Client option validation now rejects invalid timeout/retry configuration values early.
37
+ - `week.ts`: passing an empty string now throws a clear `BsuirValidationError` ("is an empty string") instead of a misleading "must be a positive integer" message.
38
+ - `filterLessons`: `weekNumber: 0` is now rejected with `assertPositiveInt` instead of silently passing.
39
+ - `http.ts`: cache eviction now uses pseudo-LRU (oldest-inserted Map key, O(1)) instead of an O(n) scan; `inFlightPromise` is typed as `Promise<T>` to eliminate unsafe `as Promise<unknown>` cast.
40
+ - `http.ts`: restored file integrity after shell heredoc substitution corrupted ES module imports; fixed `@typescript-eslint/no-unsafe-call` error on `inFlight` read by shifting the `as T` cast to the Promise type before `await`.
41
+
42
+ ### Changed
43
+
44
+ - JSDoc for `defaultRaw` client option now clarifies that a per-call `raw` option takes priority and includes an `@example` for both cases.
45
+ - Dev tooling: bumped TypeScript to 6.0.3, ESLint to ^10.2.1, Vitest and `@vitest/coverage-v8` to ^4.1.4, `@types/node` to ^25.6.0, `@typescript-eslint/*` and `typescript-eslint` to ^8.58.2, `@changesets/cli` to ^2.31.0, `@microsoft/api-extractor` to ^7.58.5, Prettier to ^3.8.3, `globals` to ^17.5.0; regenerated `package-lock.json`.
46
+ - CI now validates API report compatibility via `npm run api:report:check` (Node 24 job in matrix).
47
+ - `vitest.config.ts`: added coverage thresholds (`lines: 80`, `functions: 80`) to enforce minimum test coverage on CI.
10
48
 
11
49
  ## [0.7.0] - 2026-04-19
12
50
 
package/README.md CHANGED
@@ -38,11 +38,24 @@ const client = createBsuirClient({
38
38
  retryDelayMs: 300,
39
39
  retryMaxDelayMs: 3000,
40
40
  retryJitter: true,
41
+ cache: { ttlMs: 60_000, maxEntries: 200 },
42
+ dedupeInFlight: true,
43
+ validateResponses: false,
44
+ hooks: {
45
+ onRetry: ({ endpoint, delayMs, reason }) => {
46
+ console.log("retry", endpoint, delayMs, reason);
47
+ }
48
+ },
41
49
  defaultRaw: false
42
50
  });
43
51
  ```
44
52
 
45
53
  - `fetch` can be passed for custom runtime/testing.
54
+ - `signal` in `createBsuirClient({ signal })` acts as a global cancellation signal for all requests made by that client.
55
+ - `cache` stores successful GET responses in-memory for the configured TTL.
56
+ - `dedupeInFlight` reuses the same in-flight GET request for concurrent callers (when no per-request signal is passed).
57
+ - `validateResponses` enables runtime payload-shape checks for key endpoints.
58
+ - `hooks` provides lifecycle callbacks (`onRequest`, `onRetry`, `onResponse`, `onError`) for observability.
46
59
  - `AbortSignal` is supported by all read methods.
47
60
 
48
61
  ## API
@@ -84,7 +97,8 @@ When IIS responds with HTTP `404` or `400` (no list, missing resource, or endpoi
84
97
  SDK throws typed errors:
85
98
 
86
99
  - `BsuirApiError` for HTTP errors (contains `status`, `endpoint`, `body`). **Exception:** `client.announcements.byEmployee` / `byDepartment` resolve to `[]` on IIS HTTP `404` or `400` instead of throwing (see Announcements above).
87
- - `BsuirNetworkError` for transport errors (contains `endpoint`, `causeError`, and standard `cause`)
100
+ - `BsuirNetworkError` for transport errors (contains `endpoint` and standard `cause`)
101
+ - `BsuirResponseValidationError` for invalid payload shapes when `validateResponses: true`
88
102
  - `BsuirTimeoutError` for timeouts (contains `endpoint`, `timeoutMs`)
89
103
  - `BsuirValidationError` for invalid input parameters
90
104
  - `BsuirConfigurationError` when the runtime has no `fetch` and none was passed to `createBsuirClient({ fetch })`
@@ -151,6 +165,7 @@ npm run lint
151
165
  npm run lint:fix
152
166
  npm run check
153
167
  npm run build
168
+ npm run api:report
154
169
  ```
155
170
 
156
171
  `npm run build` uses [tsup](https://tsup.egoist.dev/) with [`experimentalDts`](https://tsup.egoist.dev/) so `.d.ts` output is produced via `@microsoft/api-extractor` rather than the legacy Rollup declaration path (which is awkward with TypeScript 6’s `baseUrl` deprecation). TypeScript’s handbook notes that [`paths` can be used without `baseUrl`](https://www.typescriptlang.org/docs/handbook/modules/reference.html) when you need path mapping.
@@ -21,6 +21,10 @@ export { ApiDateResponse }
21
21
  export { ApiDateResponse as ApiDateResponse_alias_1 }
22
22
  export { ApiDateResponse as ApiDateResponse_alias_2 }
23
23
 
24
+ export declare function assertApiDateResponse(payload: unknown, endpoint: string): asserts payload is ApiDateResponse;
25
+
26
+ export declare function assertArrayResponse(payload: unknown, endpoint: string): asserts payload is unknown[];
27
+
24
28
  export declare function assertEmployeeUrlId(value: unknown, fieldName?: string): asserts value is string;
25
29
 
26
30
  export declare function assertGroupNumber(value: unknown, fieldName?: string): asserts value is string;
@@ -29,6 +33,8 @@ export declare function assertNonEmptyString(value: unknown, fieldName: string):
29
33
 
30
34
  export declare function assertPositiveInt(value: unknown, fieldName: string): asserts value is number;
31
35
 
36
+ export declare function assertScheduleResponse(payload: unknown, endpoint: string): asserts payload is ScheduleResponse;
37
+
32
38
  declare interface Auditory {
33
39
  id: number;
34
40
  name: string;
@@ -71,18 +77,21 @@ export { BsuirApiError }
71
77
  export { BsuirApiError as BsuirApiError_alias_1 }
72
78
 
73
79
  /**
74
- * Public client contract returned by {@link createBsuirClient}.
80
+ * Public client contract returned by `createBsuirClient`.
81
+ * Use `BsuirClientShape<true>` or `BsuirClientShape<false>` for typed overloads.
75
82
  */
76
83
  declare type BsuirClient = ReturnType<typeof createBsuirClient>;
77
84
  export { BsuirClient }
78
85
  export { BsuirClient as BsuirClient_alias_1 }
79
86
 
80
87
  /**
81
- * Options accepted by {@link createBsuirClient}.
88
+ * Options accepted by `createBsuirClient`.
82
89
  */
83
90
  declare interface BsuirClientOptions {
84
91
  baseUrl?: string;
85
92
  fetch?: typeof globalThis.fetch;
93
+ /** Optional global signal to cancel all client requests. */
94
+ signal?: AbortSignal;
86
95
  /** Request timeout per attempt, in milliseconds. */
87
96
  timeoutMs?: number;
88
97
  /** Number of retry attempts for retriable GET failures. */
@@ -95,12 +104,70 @@ declare interface BsuirClientOptions {
95
104
  retryJitter?: boolean;
96
105
  /** Optional User-Agent header (used mainly in Node.js runtimes). */
97
106
  userAgent?: string;
98
- /** Force raw API payload for schedule endpoints by default. */
107
+ /** In-memory cache configuration for successful GET responses. */
108
+ cache?: CacheOptions;
109
+ /** Enables in-flight GET request deduplication by URL. */
110
+ dedupeInFlight?: boolean;
111
+ /** Enables runtime validation of API response shapes. */
112
+ validateResponses?: boolean;
113
+ /** Lifecycle hooks for request/response/retry/error events. */
114
+ hooks?: ClientHooks;
115
+ /**
116
+ * Force raw API payload for schedule endpoints by default.
117
+ * This changes return types for `schedule.getGroup/getEmployee` when `raw` is omitted.
118
+ *
119
+ * Per-call `raw` option always takes precedence over this default.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const client = createBsuirClient({ defaultRaw: true });
124
+ * // Returns ScheduleResponse (raw)
125
+ * const raw = await client.schedule.getGroup("053503");
126
+ * // Returns NormalizedScheduleResponse (per-call override)
127
+ * const normalized = await client.schedule.getGroup("053503", { raw: false });
128
+ * ```
129
+ */
99
130
  defaultRaw?: boolean;
100
131
  }
101
132
  export { BsuirClientOptions }
102
133
  export { BsuirClientOptions as BsuirClientOptions_alias_1 }
103
134
 
135
+ /**
136
+ * Fully-typed public shape of the BSUIR API client.
137
+ * All module types are inlined so API Extractor never needs to reach into private helpers.
138
+ *
139
+ * `TRawDefault` controls the default return type of
140
+ * `schedule.getGroup` / `schedule.getEmployee` when the per-call `raw` option is omitted:
141
+ * - `false` (default) → returns `NormalizedScheduleResponse`
142
+ * - `true` → returns `ScheduleResponse` (raw API payload)
143
+ *
144
+ * Per-call `raw` always takes precedence over this default.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * // Default (normalized):
149
+ * const client = createBsuirClient();
150
+ * const norm = await client.schedule.getGroup("053503"); // NormalizedScheduleResponse
151
+ *
152
+ * // Raw by default:
153
+ * const rawClient = createBsuirClient({ defaultRaw: true });
154
+ * const raw = await rawClient.schedule.getGroup("053503"); // ScheduleResponse
155
+ *
156
+ * // Per-call override (always wins):
157
+ * const override = await client.schedule.getGroup("053503", { raw: true }); // ScheduleResponse
158
+ * ```
159
+ */
160
+ export declare interface BsuirClientShape<TRawDefault extends boolean> {
161
+ schedule: ReturnType<typeof createScheduleModule<TRawDefault>>;
162
+ groups: ReturnType<typeof createGroupsModule>;
163
+ employees: ReturnType<typeof createEmployeesModule>;
164
+ faculties: ReturnType<typeof createFacultiesModule>;
165
+ departments: ReturnType<typeof createDepartmentsModule>;
166
+ specialities: ReturnType<typeof createSpecialitiesModule>;
167
+ announcements: ReturnType<typeof createAnnouncementsModule>;
168
+ auditories: ReturnType<typeof createAuditoriesModule>;
169
+ }
170
+
104
171
  declare class BsuirConfigurationError extends Error {
105
172
  constructor(message: string);
106
173
  }
@@ -109,16 +176,22 @@ export { BsuirConfigurationError as BsuirConfigurationError_alias_1 }
109
176
 
110
177
  declare class BsuirNetworkError extends Error {
111
178
  readonly endpoint: string;
112
- readonly causeError: unknown;
113
- constructor(message: string, endpoint: string, causeError: unknown);
179
+ constructor(message: string, endpoint: string, cause: unknown);
114
180
  }
115
181
  export { BsuirNetworkError }
116
182
  export { BsuirNetworkError as BsuirNetworkError_alias_1 }
117
183
 
184
+ declare class BsuirResponseValidationError extends Error {
185
+ readonly endpoint: string;
186
+ constructor(message: string, endpoint: string);
187
+ }
188
+ export { BsuirResponseValidationError }
189
+ export { BsuirResponseValidationError as BsuirResponseValidationError_alias_1 }
190
+
118
191
  declare class BsuirTimeoutError extends Error {
119
192
  readonly endpoint: string;
120
193
  readonly timeoutMs: number;
121
- constructor(message: string, endpoint: string, timeoutMs: number);
194
+ constructor(message: string, endpoint: string, timeoutMs: number, cause?: unknown);
122
195
  }
123
196
  export { BsuirTimeoutError }
124
197
  export { BsuirTimeoutError as BsuirTimeoutError_alias_1 }
@@ -137,7 +210,29 @@ export { BuildingNumber }
137
210
  export { BuildingNumber as BuildingNumber_alias_1 }
138
211
  export { BuildingNumber as BuildingNumber_alias_2 }
139
212
 
140
- export declare function createAnnouncementsModule(config: InternalClientConfig): {
213
+ declare interface CacheOptions {
214
+ /**
215
+ * Cache TTL for successful GET responses, in milliseconds.
216
+ */
217
+ ttlMs: number;
218
+ /**
219
+ * Maximum number of cached entries kept in memory.
220
+ */
221
+ maxEntries?: number;
222
+ }
223
+ export { CacheOptions }
224
+ export { CacheOptions as CacheOptions_alias_1 }
225
+
226
+ declare interface ClientHooks {
227
+ onRequest?: (context: RequestHookContext) => void;
228
+ onRetry?: (context: RetryHookContext) => void;
229
+ onResponse?: (context: ResponseHookContext) => void;
230
+ onError?: (context: ErrorHookContext) => void;
231
+ }
232
+ export { ClientHooks }
233
+ export { ClientHooks as ClientHooks_alias_1 }
234
+
235
+ declare function createAnnouncementsModule(config: Readonly<InternalClientConfig>): {
141
236
  /**
142
237
  * Lists announcements for an employee. IIS may return HTTP `404` or `400` (no list / endpoint quirks); the SDK maps those to `[]`.
143
238
  */
@@ -147,8 +242,10 @@ export declare function createAnnouncementsModule(config: InternalClientConfig):
147
242
  */
148
243
  byDepartment(id: number, options?: ReadOptions): Promise<Announcement[]>;
149
244
  };
245
+ export { createAnnouncementsModule }
246
+ export { createAnnouncementsModule as createAnnouncementsModule_alias_1 }
150
247
 
151
- export declare function createAuditoriesModule(config: InternalClientConfig): {
248
+ declare function createAuditoriesModule(config: Readonly<InternalClientConfig>): {
152
249
  /**
153
250
  * Returns the full list of auditories from `/auditories`.
154
251
  * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
@@ -159,63 +256,39 @@ export declare function createAuditoriesModule(config: InternalClientConfig): {
159
256
  */
160
257
  listAll(options?: ReadOptions): Promise<Auditory[]>;
161
258
  };
259
+ export { createAuditoriesModule }
260
+ export { createAuditoriesModule as createAuditoriesModule_alias_1 }
162
261
 
163
262
  /**
164
263
  * Creates a configured BSUIR IIS API client.
264
+ *
265
+ * Pass `{ defaultRaw: true }` to switch the default return shape of
266
+ * `schedule.getGroup` and `schedule.getEmployee` from `NormalizedScheduleResponse`
267
+ * to the raw `ScheduleResponse`. Per-call `raw` option always takes precedence.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * // Normalized (default):
272
+ * const client = createBsuirClient();
273
+ *
274
+ * // Raw by default:
275
+ * const rawClient = createBsuirClient({ defaultRaw: true });
276
+ *
277
+ * // Custom fetch + timeout:
278
+ * const client = createBsuirClient({ fetch: myFetch, timeoutMs: 5_000 });
279
+ * ```
165
280
  */
166
- declare function createBsuirClient(options?: BsuirClientOptions): {
167
- schedule: {
168
- getGroup: <TRaw extends boolean | undefined = undefined>(groupNumber: string, options?: ReadOptions & {
169
- raw?: TRaw;
170
- }) => Promise<TRaw extends true ? ScheduleResponse : NormalizedScheduleResponse>;
171
- getEmployee: <TRaw extends boolean | undefined = undefined>(urlId: string, options?: ReadOptions & {
172
- raw?: TRaw;
173
- }) => Promise<TRaw extends true ? ScheduleResponse : NormalizedScheduleResponse>;
174
- getGroupFiltered: (groupNumber: string, filter: ScheduleFilterOptions, options?: ReadOptions) => Promise<FlattenedScheduleItem[]>;
175
- getEmployeeFiltered: (urlId: string, filter: ScheduleFilterOptions, options?: ReadOptions) => Promise<FlattenedScheduleItem[]>;
176
- getGroupExams(groupNumber: string, options?: ReadOptions): Promise<FlattenedScheduleItem[]>;
177
- getEmployeeExams(urlId: string, options?: ReadOptions): Promise<FlattenedScheduleItem[]>;
178
- getGroupBySubgroup(groupNumber: string, subgroup: number, options?: ReadOptions): Promise<FlattenedScheduleItem[]>;
179
- getEmployeeBySubgroup(urlId: string, subgroup: number, options?: ReadOptions): Promise<FlattenedScheduleItem[]>;
180
- getCurrentWeek: (options?: ReadOptions) => Promise<number>;
181
- getLastUpdateByGroup(params: {
182
- groupNumber: string;
183
- } | {
184
- id: number;
185
- }, options?: ReadOptions): Promise<ApiDateResponse>;
186
- getLastUpdateByEmployee(params: {
187
- urlId: string;
188
- } | {
189
- id: number;
190
- }, options?: ReadOptions): Promise<ApiDateResponse>;
191
- };
192
- groups: {
193
- listAll(options?: ReadOptions): Promise<StudentGroupCatalogItem[]>;
194
- };
195
- employees: {
196
- listAll(options?: ReadOptions): Promise<EmployeeCatalogItem[]>;
197
- };
198
- faculties: {
199
- listAll(options?: ReadOptions): Promise<Faculty[]>;
200
- };
201
- departments: {
202
- listAll(options?: ReadOptions): Promise<Department[]>;
203
- };
204
- specialities: {
205
- listAll(options?: ReadOptions): Promise<Speciality[]>;
206
- };
207
- announcements: {
208
- byEmployee(urlId: string, options?: ReadOptions): Promise<Announcement[]>;
209
- byDepartment(id: number, options?: ReadOptions): Promise<Announcement[]>;
210
- };
211
- auditories: {
212
- listAll(options?: ReadOptions): Promise<Auditory[]>;
213
- };
214
- };
281
+ declare function createBsuirClient(options: BsuirClientOptions & {
282
+ defaultRaw: true;
283
+ }): BsuirClientShape<true>;
284
+
285
+ declare function createBsuirClient(options?: BsuirClientOptions & {
286
+ defaultRaw?: false | undefined;
287
+ }): BsuirClientShape<false>;
215
288
  export { createBsuirClient }
216
289
  export { createBsuirClient as createBsuirClient_alias_1 }
217
290
 
218
- export declare function createDepartmentsModule(config: InternalClientConfig): {
291
+ declare function createDepartmentsModule(config: Readonly<InternalClientConfig>): {
219
292
  /**
220
293
  * Returns the full list of departments from `/departments`.
221
294
  * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
@@ -226,8 +299,10 @@ export declare function createDepartmentsModule(config: InternalClientConfig): {
226
299
  */
227
300
  listAll(options?: ReadOptions): Promise<Department[]>;
228
301
  };
302
+ export { createDepartmentsModule }
303
+ export { createDepartmentsModule as createDepartmentsModule_alias_1 }
229
304
 
230
- export declare function createEmployeesModule(config: InternalClientConfig): {
305
+ declare function createEmployeesModule(config: Readonly<InternalClientConfig>): {
231
306
  /**
232
307
  * Returns the full list of employees from `/employees/all`.
233
308
  * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
@@ -238,8 +313,10 @@ export declare function createEmployeesModule(config: InternalClientConfig): {
238
313
  */
239
314
  listAll(options?: ReadOptions): Promise<EmployeeCatalogItem[]>;
240
315
  };
316
+ export { createEmployeesModule }
317
+ export { createEmployeesModule as createEmployeesModule_alias_1 }
241
318
 
242
- export declare function createFacultiesModule(config: InternalClientConfig): {
319
+ declare function createFacultiesModule(config: Readonly<InternalClientConfig>): {
243
320
  /**
244
321
  * Returns the full list of faculties from `/faculties`.
245
322
  * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
@@ -250,8 +327,10 @@ export declare function createFacultiesModule(config: InternalClientConfig): {
250
327
  */
251
328
  listAll(options?: ReadOptions): Promise<Faculty[]>;
252
329
  };
330
+ export { createFacultiesModule }
331
+ export { createFacultiesModule as createFacultiesModule_alias_1 }
253
332
 
254
- export declare function createGroupsModule(config: InternalClientConfig): {
333
+ declare function createGroupsModule(config: Readonly<InternalClientConfig>): {
255
334
  /**
256
335
  * Returns the full list of student groups from `/student-groups`.
257
336
  * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
@@ -262,16 +341,18 @@ export declare function createGroupsModule(config: InternalClientConfig): {
262
341
  */
263
342
  listAll(options?: ReadOptions): Promise<StudentGroupCatalogItem[]>;
264
343
  };
344
+ export { createGroupsModule }
345
+ export { createGroupsModule as createGroupsModule_alias_1 }
265
346
 
266
347
  export declare function createJsonResponse({ status, headers, body }: MockResponseInit): Response;
267
348
 
268
- export declare function createScheduleModule(config: InternalClientConfig): {
349
+ declare function createScheduleModule<TRawDefault extends boolean>(config: Readonly<InternalClientConfig<TRawDefault>>): {
269
350
  getGroup: <TRaw extends boolean | undefined = undefined>(groupNumber: string, options?: ReadOptions & {
270
351
  raw?: TRaw;
271
- }) => Promise<TRaw extends true ? ScheduleResponse : NormalizedScheduleResponse>;
352
+ }) => Promise<ScheduleResponseByRawOption<TRaw, TRawDefault>>;
272
353
  getEmployee: <TRaw extends boolean | undefined = undefined>(urlId: string, options?: ReadOptions & {
273
354
  raw?: TRaw;
274
- }) => Promise<TRaw extends true ? ScheduleResponse : NormalizedScheduleResponse>;
355
+ }) => Promise<ScheduleResponseByRawOption<TRaw, TRawDefault>>;
275
356
  getGroupFiltered: (groupNumber: string, filter: ScheduleFilterOptions, options?: ReadOptions) => Promise<FlattenedScheduleItem[]>;
276
357
  getEmployeeFiltered: (urlId: string, filter: ScheduleFilterOptions, options?: ReadOptions) => Promise<FlattenedScheduleItem[]>;
277
358
  getGroupExams(groupNumber: string, options?: ReadOptions): Promise<FlattenedScheduleItem[]>;
@@ -298,8 +379,10 @@ export declare function createScheduleModule(config: InternalClientConfig): {
298
379
  id: number;
299
380
  }, options?: ReadOptions): Promise<ApiDateResponse>;
300
381
  };
382
+ export { createScheduleModule }
383
+ export { createScheduleModule as createScheduleModule_alias_1 }
301
384
 
302
- export declare function createSpecialitiesModule(config: InternalClientConfig): {
385
+ declare function createSpecialitiesModule(config: Readonly<InternalClientConfig>): {
303
386
  /**
304
387
  * Returns the full list of specialities from `/specialities`.
305
388
  * If the caller aborts `options.signal`, the platform propagates `AbortError` (not wrapped by the SDK).
@@ -310,6 +393,8 @@ export declare function createSpecialitiesModule(config: InternalClientConfig):
310
393
  */
311
394
  listAll(options?: ReadOptions): Promise<Speciality[]>;
312
395
  };
396
+ export { createSpecialitiesModule }
397
+ export { createSpecialitiesModule as createSpecialitiesModule_alias_1 }
313
398
 
314
399
  export declare const default_alias: Options | Options[] | ((overrideOptions: Options) => Options | Options[] | Promise<Options | Options[]>);
315
400
 
@@ -367,6 +452,13 @@ export { EmployeeCatalogItem }
367
452
  export { EmployeeCatalogItem as EmployeeCatalogItem_alias_1 }
368
453
  export { EmployeeCatalogItem as EmployeeCatalogItem_alias_2 }
369
454
 
455
+ declare interface ErrorHookContext extends RequestHookContext {
456
+ durationMs: number;
457
+ error: unknown;
458
+ }
459
+ export { ErrorHookContext }
460
+ export { ErrorHookContext as ErrorHookContext_alias_1 }
461
+
370
462
  declare interface Faculty {
371
463
  id: number;
372
464
  name: string;
@@ -376,7 +468,27 @@ export { Faculty }
376
468
  export { Faculty as Faculty_alias_1 }
377
469
  export { Faculty as Faculty_alias_2 }
378
470
 
379
- declare type FlattenedLessonsByDay = Partial<Record<Weekday, FlattenedScheduleItem[]>>;
471
+ /**
472
+ * Filters normalized schedule lessons by specified criteria.
473
+ *
474
+ * @param response - Normalized schedule response containing lessons
475
+ * @param filter - Filter options with optional fields: source, weekday, weekNumber, subgroup,
476
+ * lessonTypeAbbrev, subjectQuery, employeeUrlId, auditory
477
+ * @returns Array of lessons matching all provided filter criteria
478
+ *
479
+ * @example
480
+ * ```ts
481
+ * const schedule = await client.schedule.getGroup("053503");
482
+ * const mondayLessons = filterLessons(schedule, { weekday: "Понедельник" });
483
+ * const practiceLessons = filterLessons(schedule, { lessonTypeAbbrev: "пр" });
484
+ * const lectureLessons = filterLessons(schedule, { lessonTypeAbbrev: ["лк", "лекция"] });
485
+ * ```
486
+ */
487
+ declare function filterLessons(response: NormalizedScheduleResponse, filter: ScheduleFilterOptions): FlattenedScheduleItem[];
488
+ export { filterLessons }
489
+ export { filterLessons as filterLessons_alias_1 }
490
+
491
+ declare type FlattenedLessonsByDay = Record<Weekday, FlattenedScheduleItem[]>;
380
492
  export { FlattenedLessonsByDay }
381
493
  export { FlattenedLessonsByDay as FlattenedLessonsByDay_alias_1 }
382
494
  export { FlattenedLessonsByDay as FlattenedLessonsByDay_alias_2 }
@@ -389,16 +501,28 @@ export { FlattenedScheduleItem }
389
501
  export { FlattenedScheduleItem as FlattenedScheduleItem_alias_1 }
390
502
  export { FlattenedScheduleItem as FlattenedScheduleItem_alias_2 }
391
503
 
392
- export declare interface InternalClientConfig {
504
+ export declare interface InternalClientConfig<TRawDefault extends boolean = boolean> {
393
505
  baseUrl: string;
394
506
  fetchImpl: typeof globalThis.fetch;
507
+ signal: AbortSignal | undefined;
395
508
  timeoutMs: number;
396
509
  retries: number;
397
510
  retryDelayMs: number;
398
511
  retryMaxDelayMs: number;
399
512
  retryJitter: boolean;
400
513
  userAgent: string | undefined;
401
- defaultRaw: boolean;
514
+ cacheTtlMs: number | undefined;
515
+ cacheMaxEntries: number;
516
+ dedupeInFlight: boolean;
517
+ validateResponses: boolean;
518
+ hooks: ClientHooks;
519
+ responseCache: Map<string, {
520
+ expiresAt: number;
521
+ value: unknown;
522
+ accessedAt: number;
523
+ }>;
524
+ inFlightRequests: Map<string, Promise<unknown>>;
525
+ defaultRaw: TRawDefault;
402
526
  }
403
527
 
404
528
  export declare function isAbortError(error: unknown): boolean;
@@ -420,14 +544,21 @@ export { Maybe as Maybe_alias_1 }
420
544
  export { Maybe as Maybe_alias_2 }
421
545
 
422
546
  /**
423
- * Combines an optional caller {@link AbortSignal} with a per-attempt timeout.
547
+ * Combines multiple abort signals and/or a timeout into a single signal.
424
548
  * When `AbortSignal.any` exists at runtime, delegates to the platform implementation.
425
- * Otherwise uses a manual merge so the timeout still applies alongside a caller signal.
549
+ * Otherwise uses a manual merge so all signals are respected.
550
+ *
551
+ * @param signalsOrFirst - Array of signals to merge, or a single signal (legacy signature)
552
+ * @param timeoutMs - Optional timeout in milliseconds to include as an additional signal
553
+ *
554
+ * @remarks
555
+ * Calling with no signals and no timeout (e.g. `mergeSignals([])`) returns a signal
556
+ * that is never aborted — the caller is responsible for not doing this intentionally.
426
557
  */
427
- export declare function mergeSignals(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal;
558
+ export declare function mergeSignals(signalsOrFirst: AbortSignal[] | (AbortSignal | undefined), timeoutMs?: number): AbortSignal;
428
559
 
429
560
  /** Used when `AbortSignal.any` is unavailable; exposed for unit tests. */
430
- export declare function mergeSignalsManual(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal;
561
+ export declare function mergeSignalsManual(signals: AbortSignal[], timeoutMs?: number): AbortSignal;
431
562
 
432
563
  export declare function mockFetchSequence(responses: Array<Response | Error>): typeof globalThis.fetch;
433
564
 
@@ -449,6 +580,31 @@ export { NormalizedScheduleResponse }
449
580
  export { NormalizedScheduleResponse as NormalizedScheduleResponse_alias_1 }
450
581
  export { NormalizedScheduleResponse as NormalizedScheduleResponse_alias_2 }
451
582
 
583
+ /**
584
+ * Transforms raw API schedule response into a normalized structure with flattened lessons.
585
+ *
586
+ * Raw API response contains lessons grouped by weekday (`schedules` object with day keys)
587
+ * and exams in a separate array. This function flattens them into a single `lessons` array
588
+ * for easier filtering and iteration, while preserving day-grouped view in `lessonsByDay`.
589
+ *
590
+ * @param response - Raw schedule response from API
591
+ * @returns Normalized schedule with additional computed fields: `lessons` (flattened array),
592
+ * `lessonsByDay` (grouped by weekday), `scheduleLessons`, and `examLessons`
593
+ *
594
+ * @example
595
+ * ```ts
596
+ * const rawSchedule = await client.schedule.getGroup("053503", { raw: true });
597
+ * const normalized = normalizeSchedule(rawSchedule);
598
+ * console.log(normalized.lessons.length); // All lessons + exams flattened
599
+ * console.log(normalized.lessonsByDay["Понедельник"]); // Monday-only lessons
600
+ * console.log(normalized.scheduleLessons.length); // Only regular schedule
601
+ * console.log(normalized.examLessons.length); // Only exams
602
+ * ```
603
+ */
604
+ declare function normalizeSchedule(response: ScheduleResponse): NormalizedScheduleResponse;
605
+ export { normalizeSchedule }
606
+ export { normalizeSchedule as normalizeSchedule_alias_1 }
607
+
452
608
  /**
453
609
  * Normalizes current week payload from API to a positive integer.
454
610
  * API can return plain text (`"1\n"`) or number.
@@ -464,7 +620,11 @@ export declare type QueryValue = string | number | boolean | null | undefined;
464
620
  /**
465
621
  * Read options used by all module methods.
466
622
  */
467
- declare interface ReadOptions extends RequestOptions {
623
+ declare interface ReadOptions {
624
+ /**
625
+ * Optional signal to cancel request from the caller side.
626
+ */
627
+ signal?: AbortSignal | undefined;
468
628
  /**
469
629
  * When true, return raw API payload where supported.
470
630
  */
@@ -473,10 +633,23 @@ declare interface ReadOptions extends RequestOptions {
473
633
  export { ReadOptions }
474
634
  export { ReadOptions as ReadOptions_alias_1 }
475
635
 
476
- export declare function requestJson<T>(config: InternalClientConfig, path: string, options?: RequestOptions): Promise<T>;
636
+ declare interface RequestHookContext {
637
+ method: RequestMethod;
638
+ path: string;
639
+ endpoint: string;
640
+ attempt: number;
641
+ maxAttempts: number;
642
+ query: QueryParams | undefined;
643
+ }
644
+ export { RequestHookContext }
645
+ export { RequestHookContext as RequestHookContext_alias_1 }
646
+
647
+ export declare function requestJson<T>(config: Readonly<InternalClientConfig>, path: string, options?: RequestOptions): Promise<T>;
648
+
649
+ export declare type RequestMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
477
650
 
478
651
  /**
479
- * Common request options shared by read-only API methods.
652
+ * Low-level HTTP request options used by internal request pipeline.
480
653
  */
481
654
  declare interface RequestOptions {
482
655
  /**
@@ -487,10 +660,38 @@ declare interface RequestOptions {
487
660
  * Optional signal to cancel request from the caller side.
488
661
  */
489
662
  signal?: AbortSignal | undefined;
663
+ /**
664
+ * HTTP method. Defaults to `GET`.
665
+ */
666
+ method?: RequestMethod | undefined;
667
+ /**
668
+ * JSON body payload for mutating requests.
669
+ */
670
+ body?: unknown;
671
+ /**
672
+ * Additional request headers.
673
+ */
674
+ headers?: HeadersInit | undefined;
490
675
  }
491
676
  export { RequestOptions }
492
677
  export { RequestOptions as RequestOptions_alias_1 }
493
678
 
679
+ declare interface ResponseHookContext extends RequestHookContext {
680
+ status: number;
681
+ durationMs: number;
682
+ fromCache: boolean;
683
+ }
684
+ export { ResponseHookContext }
685
+ export { ResponseHookContext as ResponseHookContext_alias_1 }
686
+
687
+ declare interface RetryHookContext extends RequestHookContext {
688
+ delayMs: number;
689
+ reason: "http_status" | "network_error";
690
+ status: number | undefined;
691
+ }
692
+ export { RetryHookContext }
693
+ export { RetryHookContext as RetryHookContext_alias_1 }
694
+
494
695
  declare interface ScheduleFilterOptions {
495
696
  source?: "schedules" | "exams";
496
697
  weekday?: Weekday;
@@ -515,7 +716,7 @@ declare interface ScheduleItem {
515
716
  subject: string;
516
717
  subjectFullName: string;
517
718
  note: Maybe<string>;
518
- lessonTypeAbbrev: string | null;
719
+ lessonTypeAbbrev: Maybe<string>;
519
720
  dateLesson: Maybe<string>;
520
721
  startLessonDate: Maybe<string>;
521
722
  endLessonDate: Maybe<string>;
@@ -541,6 +742,8 @@ export { ScheduleResponse }
541
742
  export { ScheduleResponse as ScheduleResponse_alias_1 }
542
743
  export { ScheduleResponse as ScheduleResponse_alias_2 }
543
744
 
745
+ declare type ScheduleResponseByRawOption<TRaw extends boolean | undefined, TRawDefault extends boolean> = TRaw extends true ? ScheduleResponse : TRaw extends false ? NormalizedScheduleResponse : TRawDefault extends true ? ScheduleResponse : NormalizedScheduleResponse;
746
+
544
747
  declare interface Speciality {
545
748
  id: number;
546
749
  name: string;