refactor(timeline): O(1) lookups in YearBand row assembly
`loose.includes(entry)` ran once per LETTER inside the band loop — O(L²) on a dense band of hundreds of loose letters, recomputed on every layer re-render. splitYearLetters now also returns its `byEvent` map, so a letter's disposition is `byEvent.has(linkedEventId)` and an event's card is `byEvent.get(eventId)`, both O(1); `consumed` is a plain object. No behavior change. Fixes review finding #3. Refs #850 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -54,11 +54,15 @@ const split = $derived(
|
|||||||
);
|
);
|
||||||
const loose = $derived(split.loose);
|
const loose = $derived(split.loose);
|
||||||
const dense = $derived(isDense(loose.length));
|
const dense = $derived(isDense(loose.length));
|
||||||
|
// Clusters keyed by eventId (built once in splitYearLetters): row assembly looks a letter's
|
||||||
|
// disposition up in O(1) — `byEvent.has(linkedEventId)` — instead of scanning the loose array
|
||||||
|
// per letter (was O(L²) on a dense band), and resolves an event's card with `byEvent.get`.
|
||||||
|
const byEvent = $derived(split.byEvent);
|
||||||
|
|
||||||
const rows = $derived.by<Row[]>(() => {
|
const rows = $derived.by<Row[]>(() => {
|
||||||
const out: Row[] = [];
|
const out: Row[] = [];
|
||||||
const { clusters } = split;
|
const { clusters } = split;
|
||||||
const consumed: string[] = [];
|
const consumed: Record<string, true> = {};
|
||||||
let stripInserted = false;
|
let stripInserted = false;
|
||||||
let letterIndex = 0;
|
let letterIndex = 0;
|
||||||
|
|
||||||
@@ -67,16 +71,19 @@ const rows = $derived.by<Row[]>(() => {
|
|||||||
// A curated event whose letters live in THIS band becomes the contained card's
|
// A curated event whose letters live in THIS band becomes the contained card's
|
||||||
// header — its title reads once, no separate pill (REQ-002). Otherwise it stays a
|
// header — its title reads once, no separate pill (REQ-002). Otherwise it stays a
|
||||||
// plain pill/world-band (REQ-005).
|
// plain pill/world-band (REQ-005).
|
||||||
const cluster = entry.eventId ? clusters.find((c) => c.eventId === entry.eventId) : undefined;
|
const cluster = entry.eventId ? byEvent.get(entry.eventId) : undefined;
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
out.push({ t: 'eventcard', event: entry, cluster });
|
out.push({ t: 'eventcard', event: entry, cluster });
|
||||||
consumed.push(cluster.eventId);
|
consumed[cluster.eventId] = true;
|
||||||
} else {
|
} else {
|
||||||
out.push({ t: 'event', entry });
|
out.push({ t: 'event', entry });
|
||||||
}
|
}
|
||||||
} else if (loose.includes(entry)) {
|
} else if (
|
||||||
// A loose letter: alternate while sparse, or fold the whole loose set into one
|
entry.kind === 'LETTER' &&
|
||||||
// density strip (inserted once, at the first loose letter) when dense.
|
!(entry.linkedEventId && byEvent.has(entry.linkedEventId))
|
||||||
|
) {
|
||||||
|
// A loose letter (not clustered): alternate while sparse, or fold the whole loose set
|
||||||
|
// into one density strip (inserted once, at the first loose letter) when dense.
|
||||||
if (!dense) {
|
if (!dense) {
|
||||||
out.push({ t: 'letter', entry, side: letterIndex % 2 === 0 ? 'left' : 'right' });
|
out.push({ t: 'letter', entry, side: letterIndex % 2 === 0 ? 'left' : 'right' });
|
||||||
letterIndex += 1;
|
letterIndex += 1;
|
||||||
@@ -91,7 +98,7 @@ const rows = $derived.by<Row[]>(() => {
|
|||||||
// Cross-year clusters: a cluster whose event is NOT a same-year EVENT entry renders as a
|
// Cross-year clusters: a cluster whose event is NOT a same-year EVENT entry renders as a
|
||||||
// text-header card (no pill, no edit link) holding this year's linked letters (REQ-004).
|
// text-header card (no pill, no edit link) holding this year's linked letters (REQ-004).
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
if (!consumed.includes(cluster.eventId)) {
|
if (!consumed[cluster.eventId]) {
|
||||||
out.push({ t: 'eventcard', cluster });
|
out.push({ t: 'eventcard', cluster });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export interface EventCluster {
|
|||||||
export interface SplitLetters {
|
export interface SplitLetters {
|
||||||
clusters: EventCluster[];
|
clusters: EventCluster[];
|
||||||
loose: TimelineEntryDTO[];
|
loose: TimelineEntryDTO[];
|
||||||
|
/** Clusters keyed by `eventId` for O(1) lookup during row assembly (a letter's disposition is
|
||||||
|
* `byEvent.has(linkedEventId)`; an event's card is `byEvent.get(eventId)`). */
|
||||||
|
byEvent: Map<string, EventCluster>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,5 +84,5 @@ export function splitYearLetters(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { clusters, loose };
|
return { clusters, loose, byEvent };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user