238 lines
11 KiB
Markdown
238 lines
11 KiB
Markdown
---
|
|
phase: 08-pagination-quality-cache-performance
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift
|
|
autonomous: true
|
|
requirements:
|
|
- QUAL-01
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift"
|
|
provides: "Disk-persistent RDEPUBTextBook cache with SHA256 key generation"
|
|
contains: "class RDEPUBTextBookCache"
|
|
min_lines: 100
|
|
key_links:
|
|
- from: "RDEPUBTextBookCache.swift"
|
|
to: "Library/Caches/RDEPUBTextBookCache/"
|
|
via: "FileManager.default.urls(for: .cachesDirectory)"
|
|
pattern: "cachesDirectory"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- Key types the executor must serialize. From RDEPUBTextBookBuilder.swift lines 16-94 -->
|
|
|
|
From RDEPUBTextBookBuilder.swift:
|
|
```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:
|
|
```swift
|
|
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]
|
|
}
|
|
```
|
|
|
|
<!-- File I/O pattern to follow. From RDEPUBParser+Archive.swift lines 55-66 -->
|
|
```swift
|
|
// 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)
|
|
```
|
|
|
|
<!-- NSKeyedArchiver pattern for NSAttributedString serialization -->
|
|
```swift
|
|
// 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)
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create RDEPUBTextBookCache class with cache key generation and disk I/O</name>
|
|
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift</files>
|
|
<read_first>
|
|
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
|
Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift
|
|
Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift
|
|
</read_first>
|
|
<action>
|
|
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
|
|
</action>
|
|
<verify>
|
|
<automated>xcodebuild build -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 17' 2>&1 | tail -5</automated>
|
|
</verify>
|
|
<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>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
Create `.planning/phases/08-pagination-quality-cache-performance/08-01-SUMMARY.md` when done
|
|
</output>
|