ReadViewSDK/.planning/phases/08-pagination-quality-cache-performance/08-01-PLAN.md

11 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
08-pagination-quality-cache-performance 01 execute 1
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift
true
QUAL-01
truths artifacts key_links
Same bookID + fontSize + lineHeightMultiple + contentInsets produces same cache key
Cache hit returns stored RDEPUBTextBook without calling build()
Cache miss falls through to build and stores result
Schema version bump invalidates all cached entries
Changing any layout parameter produces a different cache key
path provides contains min_lines
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift Disk-persistent RDEPUBTextBook cache with SHA256 key generation class RDEPUBTextBookCache 100
from to via pattern
RDEPUBTextBookCache.swift Library/Caches/RDEPUBTextBookCache/ FileManager.default.urls(for: .cachesDirectory) cachesDirectory
Create a disk-persistent cache for RDEPUBTextBook objects keyed by layout parameters (bookID + fontSize + lineHeightMultiple + contentInsets + pageSize), using NSKeyedArchiver for serialization and SHA256 for cache key hashing. This eliminates redundant full pagination of the same book under identical layout configuration.

Purpose: QUAL-01 requires that the same viewport and typography configuration does not trigger redundant full pagination. The cache stores complete RDEPUBTextBook objects to disk so repeated opens with the same parameters skip the entire build pipeline.

Output: A new RDEPUBTextBookCache.swift file providing load/save/invalidate operations.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-pagination-quality-cache-performance/08-CONTEXT.md @.planning/phases/08-pagination-quality-cache-performance/08-RESEARCH.md @.planning/phases/08-pagination-quality-cache-performance/08-PATTERNS.md @Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift @Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift @Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift

From RDEPUBTextBookBuilder.swift:

public struct RDEPUBTextBook: Equatable {
    public var chapters: [RDEPUBTextChapter]
    public var pages: [RDEPUBTextPage]
    public init(chapters: [RDEPUBTextChapter], pages: [RDEPUBTextPage])
}

public struct RDEPUBTextChapter: Equatable {
    public var chapterIndex: Int
    public var spineIndex: Int
    public var href: String
    public var title: String
    public var attributedContent: NSAttributedString
    public var fragmentOffsets: [String: Int]
    public var pageBreakReasons: [RDEPUBTextPageBreakReason]
    public var pages: [RDEPUBTextPage]
}

public struct RDEPUBTextPage: Equatable {
    public var absolutePageIndex: Int
    public var chapterIndex: Int
    public var spineIndex: Int
    public var href: String
    public var chapterTitle: String
    public var pageIndexInChapter: Int
    public var totalPagesInChapter: Int
    public var content: NSAttributedString
    public var contentRange: NSRange
    public var pageStartOffset: Int
    public var pageEndOffset: Int
    public var metadata: RDEPUBTextPageMetadata
}

From RDEPUBReadingModels.swift lines 183-215:

public struct RDEPUBTextPageMetadata: Codable, Equatable {
    public var breakReason: RDEPUBTextPageBreakReason
    public var blockRange: NSRange?
    public var attachmentRanges: [NSRange]
    public var attachmentKinds: [RDEPUBTextAttachmentKind]
    public var blockKinds: [RDEPUBTextBlockKind]
    public var semanticHints: [RDEPUBTextSemanticHint]
    public var attachmentPlacements: [RDEPUBTextAttachmentPlacement]
    public var trailingFragmentID: String?
    public var diagnostics: [String]
}
// Cache directory pattern:
let baseURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
    .appendingPathComponent("RDEPUBTextBookCache", isDirectory: true)
    ?? FileManager.default.temporaryDirectory.appendingPathComponent("RDEPUBTextBookCache", isDirectory: true)
try FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
// NSAttributedString supports NSCoding — serialize via NSKeyedArchiver
let data = try NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
let object = try NSKeyedUnarchiver.unarchivedObject(ofClass: SomeClass.self, from: data)
Task 1: Create RDEPUBTextBookCache class with cache key generation and disk I/O Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift Create a new file `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift` implementing the complete cache system.

RDEPUBTextBookCache class (per D-01):

  1. Cache key generation using CryptoKit SHA256:

    • Key inputs: bookID: String, fontSize: CGFloat, lineHeightMultiple: CGFloat, contentInsets: UIEdgeInsets, pageSize: CGSize, schemaVersion: Int
    • Format: SHA256("\(bookID)_\(fontSize)_\(lineHeightMultiple)_\(contentInsets.top)_\(contentInsets.left)_\(contentInsets.bottom)_\(contentInsets.right)_\(pageSize.width)_\(pageSize.height)_v\(schemaVersion)")
    • Output: hex-encoded string + ".cache" extension for use as filename
    • Reference WXRead pattern: WRChapterPageCount.currentCacheKeyWithBookId: encodes bookId + fontSize + lineSpacing + pageWidth + pageHeight
  2. Thread safety using a serial DispatchQueue:

    • private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility)
    • All read/write operations wrapped in queue.sync { }
  3. Storage directory: Library/Caches/RDEPUBTextBookCache/ using FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask), with fallback to temporaryDirectory. Create directory with createDirectory(at:withIntermediateDirectories:) on init.

  4. Serialization strategy using NSKeyedArchiver:

    • Since RDEPUBTextBook is NOT NSCoding-conformant, create NSCoding wrapper classes for serialization:
      • RDEPUBTextBookArchive: NSObject, NSCoding — stores array of chapter archives
      • RDEPUBTextChapterArchive: NSObject, NSCoding — stores attributedContent (via NSKeyedArchiver), page data arrays
      • RDEPUBTextPageArchive: NSObject, NSCoding — stores all RDEPUBTextPage fields
      • RDEPUBTextPageMetadataArchive: NSObject, NSCoding — stores RDEPUBTextPageMetadata fields
    • For NSRange fields: encode as {location: Int, length: Int} pairs
    • For enums: encode as rawValue strings
    • Wrap all NSCoding classes as private or internal (not public API)
  5. Public API:

    • public init(subdirectory: String = "RDEPUBTextBookCache") — sets up cache directory
    • public func cacheKey(bookID: String, fontSize: CGFloat, lineHeightMultiple: CGFloat, contentInsets: UIEdgeInsets, pageSize: CGSize) -> String — returns SHA256 hash filename
    • public func load(key: String) -> RDEPUBTextBook? — deserializes from disk, returns nil on miss or error
    • public func save(_ book: RDEPUBTextBook, key: String) — serializes to disk
    • public func invalidateAll() — deletes all files in cache directory
    • public var schemaVersion: Int — default 1, bump when pagination logic changes to force invalidation
  6. Error handling: All file I/O wrapped in do/catch. Failures logged via print("[Cache] ...") pattern and return nil/false (no throws in public API to avoid breaking callers).

  7. Console logging pattern (follows existing [EPUB] style):

    • [Cache] save key=... chapters=N pages=N on save
    • [Cache] load HIT key=... on hit
    • [Cache] load MISS key=... on miss
    • [Cache] invalidateAll on clear xcodebuild build -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 17' 2>&1 | tail -5 <acceptance_criteria>
    • File Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift exists and contains public final class RDEPUBTextBookCache
    • Cache key method produces deterministic SHA256 hex string from layout parameters
    • Same inputs always produce identical key (deterministic)
    • Different fontSize values produce different keys
    • schemaVersion is a public property defaulting to 1
    • load(key:) returns RDEPUBTextBook? (not throwing)
    • save(_:key:) accepts RDEPUBTextBook (not throwing)
    • invalidateAll() method exists
    • Serial dispatch queue used for thread safety (queue.sync)
    • Cache directory is under Library/Caches/RDEPUBTextBookCache/
    • NSCoding wrapper classes exist for RDEPUBTextBook/Chapter/Page/Metadata serialization
    • NSRange fields encoded as location+length integer pairs
    • Build succeeds without compiler errors </acceptance_criteria> RDEPUBTextBookCache.swift is a complete, compilable file implementing SHA256 cache key generation, NSKeyedArchiver serialization with NSCoding wrappers, serial-queue thread safety, and load/save/invalidateAll API. Build succeeds.

<threat_model>

Trust Boundaries

Boundary Description
disk-cache → memory Cached RDEPUBTextBook loaded from disk into memory; tampered cache files could inject malicious attributed strings

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation
T-08-01 Tampering RDEPUBTextBookCache disk files accept Cache stored in app sandbox Library/Caches; no external access; low-value target
T-08-02 Information Disclosure RDEPUBTextBookCache disk files accept No PII in cache; book content already accessible via app bundle
T-08-SC Tampering npm/pip/cargo installs mitigate No external packages installed in this phase
</threat_model>
- `xcodebuild build -scheme ReadViewDemo` succeeds - `RDEPUBTextBookCache.swift` contains public class with cacheKey/load/save/invalidateAll - SHA256 key generation uses CryptoKit - NSCoding wrapper classes handle NSAttributedString and NSRange serialization - Cache directory created under Library/Caches

<success_criteria>

  • New file RDEPUBTextBookCache.swift exists with complete implementation
  • Cache key is deterministic: same inputs -> same key
  • NSKeyedArchiver serialization produces valid .cache files
  • Build succeeds with no warnings related to new code </success_criteria>
Create `.planning/phases/08-pagination-quality-cache-performance/08-01-SUMMARY.md` when done