11 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-pagination-quality-cache-performance | 01 | execute | 1 |
|
true |
|
|
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.swiftFrom 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):
-
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
- Key inputs:
-
Thread safety using a serial DispatchQueue:
private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility)- All read/write operations wrapped in
queue.sync { }
-
Storage directory:
Library/Caches/RDEPUBTextBookCache/usingFileManager.default.urls(for: .cachesDirectory, in: .userDomainMask), with fallback totemporaryDirectory. Create directory withcreateDirectory(at:withIntermediateDirectories:)on init. -
Serialization strategy using NSKeyedArchiver:
- Since RDEPUBTextBook is NOT NSCoding-conformant, create NSCoding wrapper classes for serialization:
RDEPUBTextBookArchive: NSObject, NSCoding— stores array of chapter archivesRDEPUBTextChapterArchive: NSObject, NSCoding— stores attributedContent (via NSKeyedArchiver), page data arraysRDEPUBTextPageArchive: NSObject, NSCoding— stores all RDEPUBTextPage fieldsRDEPUBTextPageMetadataArchive: 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
privateorinternal(not public API)
- Since RDEPUBTextBook is NOT NSCoding-conformant, create NSCoding wrapper classes for serialization:
-
Public API:
public init(subdirectory: String = "RDEPUBTextBookCache")— sets up cache directorypublic func cacheKey(bookID: String, fontSize: CGFloat, lineHeightMultiple: CGFloat, contentInsets: UIEdgeInsets, pageSize: CGSize) -> String— returns SHA256 hash filenamepublic func load(key: String) -> RDEPUBTextBook?— deserializes from disk, returns nil on miss or errorpublic func save(_ book: RDEPUBTextBook, key: String)— serializes to diskpublic func invalidateAll()— deletes all files in cache directorypublic var schemaVersion: Int— default 1, bump when pagination logic changes to force invalidation
-
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). -
Console logging pattern (follows existing
[EPUB]style):[Cache] save key=... chapters=N pages=Non save[Cache] load HIT key=...on hit[Cache] load MISS key=...on miss[Cache] invalidateAllon clear xcodebuild build -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 17' 2>&1 | tail -5 <acceptance_criteria>- File
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swiftexists and containspublic 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
schemaVersionis a public property defaulting to 1load(key:)returnsRDEPUBTextBook?(not throwing)save(_:key:)acceptsRDEPUBTextBook(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> |
<success_criteria>
- New file
RDEPUBTextBookCache.swiftexists 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>