HFU Digital Docs
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) tuple
  • hashIcalFeed — 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:4511:15
    startTime: new Date('2026-04-13T09:45:00'),
});
// → 'a3b9c8f1d2e4576a' (16-char SHA-256 prefix)

Hash inputs

FieldTypeNotes
semesterIdstringYour internal semester ID (whatever you store)
summarystringLowercased + trimmed before hashing
weekdaynumber0 = Sunday … 6 = Saturday
studyBlocknumber | nullnull is hashed as the literal string 'unknown'
startTimeDateOnly 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.
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);

On this page