Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/core/src/enhancers/pagination/chapters/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { resolveChapterInfoFromVisibleNode } from "./node"
export { buildTocCandidatesBySpineHref, buildTocIndex } from "./shared"
export { buildChaptersInfo, buildStaticChaptersInfo } from "./static"
export {
buildChaptersInfo,
createStaticChaptersResolver,
type StaticChaptersResolver,
} from "./static"
export type { ChapterInfo, TocCandidatesBySpineHref, TocIndex } from "./types"
9 changes: 8 additions & 1 deletion packages/core/src/enhancers/pagination/chapters/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,14 @@ const flattenToc = (
})
}

const getSpineItemIndexByHref = (manifest: Manifest) => {
/**
* Map every spine href to the index of its first occurrence.
*
* Building this once lets callers resolve a spine item index by href in O(1)
* instead of scanning `manifest.spineItems` per lookup. First-occurrence wins,
* which matches `Array.prototype.findIndex` semantics for duplicate hrefs.
*/
export const getSpineItemIndexByHref = (manifest: Manifest) => {
const indexByHref = new Map<string, number>()

manifest.spineItems.forEach((item, index) => {
Expand Down
87 changes: 58 additions & 29 deletions packages/core/src/enhancers/pagination/chapters/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Manifest } from "@prose-reader/shared"
import {
buildChapterInfoFromChain,
buildTocIndex,
getSpineItemIndexByHref,
isPossibleTocItemCandidateForHref,
stripAnchor,
} from "./shared"
Expand All @@ -26,17 +27,15 @@ const shouldSkipAnchorSubChapter = ({
const findChapterChainByHref = ({
href,
tocIndex,
manifest,
spineItemIndexByHref,
}: {
href: string
tocIndex: FlatTocEntry[]
manifest: Manifest
spineItemIndexByHref: Map<string, number>
}): TocPathEntry[] | undefined => {
const hrefWithoutAnchor = stripAnchor(href)
const hrefHasAnchor = href.includes(`#`)
const spineItemIndex = manifest.spineItems.findIndex(
(item) => item.href === hrefWithoutAnchor,
)
const spineItemIndex = spineItemIndexByHref.get(hrefWithoutAnchor) ?? -1

let bestChain: TocPathEntry[] | undefined

Expand Down Expand Up @@ -66,37 +65,67 @@ export const buildChaptersInfo = (
manifest: Manifest,
): ChapterInfo | undefined => {
const tocIndex = buildTocIndex(tocItem, manifest)
const chapterChain = findChapterChainByHref({ href, tocIndex, manifest })
const spineItemIndexByHref = getSpineItemIndexByHref(manifest)
const chapterChain = findChapterChainByHref({
href,
tocIndex,
spineItemIndexByHref,
})

return chapterChain ? buildChapterInfoFromChain(chapterChain) : undefined
}

const buildChapterInfoFromSpineItem = (
manifest: Manifest,
tocIndex: TocIndex,
item: Manifest[`spineItems`][number],
) => {
const { href } = item

const chapterChain = findChapterChainByHref({ href, tocIndex, manifest })

return chapterChain ? buildChapterInfoFromChain(chapterChain) : undefined
export type StaticChaptersResolver = {
/**
* Fallback chapter info for a spine item, resolved from its own href against
* the TOC. Returns `undefined` for spine items not present in the TOC.
*/
get: (spineItemId: string) => ChapterInfo | undefined
}

export const buildStaticChaptersInfo = (
/**
* Lazily resolve the static (href-based) fallback chapter info per spine item.
*
* This fallback is only read for the few spine items actually displayed (see
* `mapChapterInfo`), yet the previous implementation eagerly resolved it for
* *every* spine item at book open — an O(spineItems × tocEntries) pass whose
* result was mostly thrown away on large books. Resolving on demand and caching
* per id makes the cost proportional to the items the reader visits, and each
* resolution avoids an O(spineItems) `findIndex` by reusing a prebuilt
* href → index map.
*/
export const createStaticChaptersResolver = (
manifest: Manifest,
tocIndex: TocIndex,
): { [key: string]: ChapterInfo | undefined } => {
if (!manifest) return {}

const chaptersInfo = manifest.spineItems.reduce(
(acc, item) => {
acc[item.id] = buildChapterInfoFromSpineItem(manifest, tocIndex, item)

return acc
): StaticChaptersResolver => {
const spineItemIndexByHref = getSpineItemIndexByHref(manifest)

// Last write wins, matching the previous `record[item.id] = …` assignment
// semantics when several spine items share an id.
const hrefBySpineItemId = new Map<string, string>()
manifest.spineItems.forEach((item) => {
hrefBySpineItemId.set(item.id, item.href)
})

const cache = new Map<string, ChapterInfo | undefined>()

return {
get: (spineItemId) => {
const cached = cache.get(spineItemId)
if (cached !== undefined || cache.has(spineItemId)) return cached

const href = hrefBySpineItemId.get(spineItemId)
const chapterChain =
href !== undefined
? findChapterChainByHref({ href, tocIndex, spineItemIndexByHref })
: undefined
const chapterInfo = chapterChain
? buildChapterInfoFromChain(chapterChain)
: undefined

cache.set(spineItemId, chapterInfo)

return chapterInfo
},
{} as { [key: string]: ChapterInfo | undefined },
)

return chaptersInfo
}
}
13 changes: 8 additions & 5 deletions packages/core/src/enhancers/pagination/trackPaginationInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ import { Pages, type PagesState } from "../../spine/Pages"
import type { SpineItem } from "../../spineItem/SpineItem"
import type { LayoutEnhancerOutput } from "../layout/layoutEnhancer"
import {
buildStaticChaptersInfo,
buildTocCandidatesBySpineHref,
buildTocIndex,
createStaticChaptersResolver,
resolveChapterInfoFromVisibleNode,
type StaticChaptersResolver,
type TocCandidatesBySpineHref,
} from "./chapters"
import { getPercentageEstimate } from "./progression"

type ChaptersData = {
tocCandidatesBySpineHref: TocCandidatesBySpineHref
chaptersInfo: ReturnType<typeof buildStaticChaptersInfo>
chaptersInfo: StaticChaptersResolver
}

type ChapterPaginationInfo = Pick<
Expand Down Expand Up @@ -97,12 +98,14 @@ const mapChapterInfo = ({
return {
beginChapterInfo:
beginChapterInfoFromVisibleNode ??
(beginItem ? chaptersData.chaptersInfo[beginItem.item.id] : undefined),
(beginItem
? chaptersData.chaptersInfo.get(beginItem.item.id)
: undefined),
beginSpineItemReadingDirection: beginItem?.readingDirection,
beginAbsolutePageIndex: beginPageEntry?.absolutePageIndex,
endChapterInfo:
endChapterInfoFromVisibleNode ??
(endItem ? chaptersData.chaptersInfo[endItem.item.id] : undefined),
(endItem ? chaptersData.chaptersInfo.get(endItem.item.id) : undefined),
endSpineItemReadingDirection: endItem?.readingDirection,
endAbsolutePageIndex: endPageEntry?.absolutePageIndex,
}
Expand Down Expand Up @@ -186,7 +189,7 @@ const observeChaptersData = (reader: Reader & LayoutEnhancerOutput) =>

return {
tocCandidatesBySpineHref,
chaptersInfo: buildStaticChaptersInfo(manifest, tocIndex),
chaptersInfo: createStaticChaptersResolver(manifest, tocIndex),
}
}),
)
Expand Down
Loading