CourseKitStarPlan
Content-Hash Identity
Stable content hashes for tracking events across syncs even when upstream UIDs change, plus cheap iCal-feed change detection
iCal UID values are not always stable across StarPlan syncs — the upstream system can re-issue UIDs whenever the schedule is republished. This module provides two hash functions that let you detect changes without trusting UID:
generateContentHash— derive a stable per-event identity from the fields that should uniquely identify a (semester, lecture, slot, date) tuplehashIcalFeed— hash the entire raw feed for cheap "did anything change?" polling
generateContentHash
import { generateContentHash } from '@hfu.digital/coursekit-starplan';
const id = generateContentHash({
semesterId: 'SEM_2026_SS',
summary: 'Datenbanken 1',
weekday: 1, // Monday
studyBlock: 2, // 09:45 – 11:15
startTime: new Date('2026-04-13T09:45:00'),
});
// → 'a3b9c8f1d2e4576a' (16-char SHA-256 prefix)Hash inputs
| Field | Type | Notes |
|---|---|---|
semesterId | string | Your internal semester ID (whatever you store) |
summary | string | Lowercased + trimmed before hashing |
weekday | number | 0 = Sunday … 6 = Saturday |
studyBlock | number | null | null is hashed as the literal string 'unknown' |
startTime | Date | Only the date portion (YYYY-MM-DD) participates in the hash |
Properties
- Algorithm: SHA-256, truncated to the first 16 hex characters
- Stable: same logical event → same hash, every sync
- Collision-resistant at the institution scale (HFU produces ~100k events/year; truncation to 64 bits is safe at that scale)
- Deterministic across Node.js versions and platforms
hashIcalFeed
import { hashIcalFeed } from '@hfu.digital/coursekit-starplan';
const ical = await client.fetchIcal(semesterId);
const hash = hashIcalFeed(ical);
if (hash === lastSeenHash) {
// upstream is unchanged — skip parsing
return;
}Properties
- Algorithm: MD5 (deliberately cheap — this is a cache key, not a security primitive)
- Use case: poll loop short-circuit. Skip
parseIcal+ downstream work when nothing changed.
Recommended pattern
const ical = await client.fetchIcal(semesterId);
const feedHash = hashIcalFeed(ical);
if (feedHash === lastFeedHash) return; // nothing changed upstream
const parsed = parseIcal(ical);
const events = parsed.map((ev) => ({
contentHash: generateContentHash({
semesterId,
summary: ev.summary,
weekday: ev.weekday,
studyBlock: ev.studyBlock,
startTime: ev.dtstart,
}),
/* ...other fields... */
}));
const diff = detectChanges('starplan-event', previousEvents, events);
await persist(events, diff);
await saveLastSeenFeedHash(feedHash);