All files / src/lib/reading-history history.ts

96.66% Statements 29/30
82.75% Branches 24/29
100% Functions 9/9
100% Lines 27/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87                  1x 1x     39x       39x   39x 39x 39x 39x 39x 39x   39x                     8x     31x                     6x   6x   39x 89x   31x 29x 29x           7x   6x 6x 6x   1x               1x       1x    
export type ReadingHistoryEntry = {
  slug: string;
  url: string;
  title: string;
  category: string;
  visitedAt: number;
  progress: number;
};
 
export const READING_HISTORY_KEY = "zhixing:reading-history:v1";
export const READING_HISTORY_LIMIT = 20;
 
function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}
 
function normalizeEntry(value: unknown): ReadingHistoryEntry | undefined {
  Iif (!isRecord(value)) return undefined;
 
  const slug = typeof value.slug === "string" ? value.slug.trim() : "";
  const url = typeof value.url === "string" ? value.url.trim() : "";
  const title = typeof value.title === "string" ? value.title.trim() : "";
  const category = typeof value.category === "string" ? value.category.trim() : "";
  const visitedAt = value.visitedAt;
  const progress = value.progress;
 
  if (
    !slug ||
    !url.startsWith("/posts/") ||
    !title ||
    !category ||
    typeof visitedAt !== "number" ||
    !Number.isFinite(visitedAt) ||
    visitedAt < 0 ||
    typeof progress !== "number" ||
    !Number.isFinite(progress)
  ) {
    return undefined;
  }
 
  return {
    slug,
    url,
    title,
    category,
    visitedAt,
    progress: Math.round(Math.min(100, Math.max(0, progress))),
  };
}
 
function normalizeHistory(values: unknown[]): ReadingHistoryEntry[] {
  const seen = new Set<string>();
 
  return values
    .map(normalizeEntry)
    .filter((entry): entry is ReadingHistoryEntry => Boolean(entry))
    .sort((a, b) => b.visitedAt - a.visitedAt)
    .filter((entry) => {
      if (seen.has(entry.slug)) return false;
      seen.add(entry.slug);
      return true;
    })
    .slice(0, READING_HISTORY_LIMIT);
}
 
export function parseReadingHistory(raw: string | null): ReadingHistoryEntry[] {
  if (!raw) return [];
 
  try {
    const parsed: unknown = JSON.parse(raw);
    return Array.isArray(parsed) ? normalizeHistory(parsed) : [];
  } catch {
    return [];
  }
}
 
export function upsertReadingHistory(
  entries: ReadingHistoryEntry[],
  next: ReadingHistoryEntry,
): ReadingHistoryEntry[] {
  return normalizeHistory([next, ...entries]);
}
 
export function serializeReadingHistory(entries: ReadingHistoryEntry[]): string {
  return JSON.stringify(normalizeHistory(entries));
}