Skip to content

Commit bffabc5

Browse files
committed
Implement CacheHandler for caching ICS data and refactor timetable fetching logic as well as more robust error handling
1 parent ae01e86 commit bffabc5

File tree

5 files changed

+60
-31
lines changed

5 files changed

+60
-31
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "webuntis-timetable",
3-
"version": "1.0.0",
3+
"version": "0.2.0",
44
"type": "commonjs",
55
"scripts": {
66
"build": "tsc -p tsconfig.json",
@@ -14,7 +14,7 @@
1414
},
1515
"devDependencies": {
1616
"@types/express": "^5.0.3",
17-
"@types/node": "^24.3.1",
17+
"@types/node": "24.3.3",
1818
"ts-node": "^x.x.x",
1919
"typescript": "^x.x.x"
2020
}

src/cacheHandler.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { CacheEntry } from "./types";
2+
3+
export class CacheHandler {
4+
private cache = new Map<string, CacheEntry>();
5+
private ttlMs: number;
6+
7+
constructor(ttlSeconds: number) {
8+
this.ttlMs = ttlSeconds * 1000;
9+
setInterval(() => this.cleanup(), 60_000);
10+
}
11+
12+
get(user: string): CacheEntry | undefined {
13+
const entry = this.cache.get(user);
14+
if (!entry) return undefined;
15+
const now = Date.now();
16+
if (now - entry.timestamp > this.ttlMs) {
17+
this.cache.delete(user);
18+
return undefined;
19+
}
20+
return entry;
21+
}
22+
23+
set(user: string, ics: string) {
24+
this.cache.set(user, { timestamp: Date.now(), ics });
25+
}
26+
27+
private cleanup() {
28+
const now = Date.now();
29+
for (const [user, entry] of this.cache.entries()) {
30+
if (now - entry.timestamp > this.ttlMs) {
31+
this.cache.delete(user);
32+
}
33+
}
34+
}
35+
}

src/ics.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// src/ics.ts
21
import ical, { ICalEventStatus } from "ical-generator";
32
import { Lesson } from "./types";
43

src/index.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@ import { loadConfig } from "./config";
33
import { fetchTimetable } from "./webuntis";
44
import { lessonsToIcs } from "./ics";
55
import { CacheEntry } from "./types";
6+
import { sessionCache, SESSION_TTL_MS } from "./webuntis";
7+
import { CacheHandler } from "./cacheHandler";
68

79
async function main() {
810
const { config, configPath } = await loadConfig();
911
console.log(`Loaded config from ${configPath}`);
1012

1113
const app = express();
14+
const icsCache = new CacheHandler(config.cacheDuration);
1215

13-
const icsCache = new Map<string, CacheEntry>();
16+
function sendIcs(res: express.Response, filename: string, ics: string) {
17+
return res
18+
.setHeader("Content-Type", "text/calendar")
19+
.setHeader(
20+
"Content-Disposition",
21+
`attachment; filename=${filename}.ics`
22+
)
23+
.send(ics);
24+
}
1425

1526
app.get("/timetable/:name", async (req, res) => {
1627
try {
@@ -21,19 +32,9 @@ async function main() {
2132
);
2233
if (!user) return res.status(404).send("User not found");
2334

24-
const now = Date.now();
25-
const cache = icsCache.get(user.username);
26-
if (
27-
cache &&
28-
now - cache.timestamp < config.cacheDuration * 60 * 60 * 24
29-
) {
30-
return res
31-
.setHeader("Content-Type", "text/calendar")
32-
.setHeader(
33-
"Content-Disposition",
34-
`attachment; filename=${user.friendlyName}.ics`
35-
)
36-
.send(cache.ics);
35+
const cacheEntry = icsCache.get(user.username);
36+
if (cacheEntry) {
37+
return sendIcs(res, user.friendlyName, cacheEntry.ics);
3738
}
3839

3940
const today = new Date();
@@ -48,14 +49,7 @@ async function main() {
4849
config.timezone || "Europe/Berlin"
4950
);
5051

51-
icsCache.set(user.username, { timestamp: now, ics });
52-
53-
res.setHeader("Content-Type", "text/calendar");
54-
res.setHeader(
55-
"Content-Disposition",
56-
`attachment; filename=${user.friendlyName}.ics`
57-
);
58-
res.send(ics);
52+
return sendIcs(res, user.friendlyName, ics);
5953
} catch (err) {
6054
console.error(err);
6155
res.status(500).send("Error fetching timetable");

src/webuntis.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ interface SessionEntry {
88
timestamp: number;
99
}
1010

11-
const sessionCache = new Map<string, SessionEntry>();
12-
const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
11+
export const sessionCache = new Map<string, SessionEntry>();
12+
export const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
1313

1414
async function getUntisSession(user: User): Promise<WebUntis> {
1515
const cached = sessionCache.get(user.username);
@@ -41,7 +41,6 @@ export async function fetchTimetable(
4141
const untis = await getUntisSession(user);
4242

4343
try {
44-
await untis.login();
4544
const rawTimetable = await untis.getOwnTimetableForRange(
4645
startDate,
4746
endDate
@@ -59,9 +58,9 @@ export async function fetchTimetable(
5958
.map((entry: any) => ({
6059
startTime: entry.startTime,
6160
endTime: entry.endTime,
62-
subject: entry.su?.[0]?.longname || "Event",
61+
subject: entry.su?.[0]?.name || "Event",
6362
teacher: entry.te?.[0]?.name || "Unknown Teacher",
64-
room: entry.ro?.[0]?.longname || "Unknown Room",
63+
room: entry.ro?.[0]?.name || "Unknown Room",
6564
class:
6665
entry.kl?.[1]?.longname ||
6766
entry.kl?.[0]?.longname ||
@@ -70,7 +69,9 @@ export async function fetchTimetable(
7069
}));
7170

7271
return mergeLessons(lessons);
73-
} finally {
72+
} catch (error) {
7473
await untis.logout();
74+
sessionCache.delete(user.username);
75+
throw error;
7576
}
7677
}

0 commit comments

Comments
 (0)