feat(wxread): align pagination, rendering, and docs
This commit is contained in:
@@ -5,40 +5,35 @@ type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift
|
||||
autonomous: true
|
||||
requirements:
|
||||
- QUAL-01
|
||||
must_haves:
|
||||
truths:
|
||||
- "Repeated pagination of the same chapter with unchanged viewport and typography returns cached layout frames without re-typesetting"
|
||||
- "Changing font size or viewport dimensions invalidates the cache and triggers fresh pagination"
|
||||
- "Cache lookup is transparent — downstream consumers receive the same RDEPUBTextLayoutFrame array whether cached or freshly computed"
|
||||
- "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/RDEPUBTextRendererCache.swift"
|
||||
provides: "Cache key struct and cache manager for pagination layout frames"
|
||||
exports: ["RDEPUBTextRendererCache", "RDEPUBPaginationCacheKey"]
|
||||
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift"
|
||||
provides: "Cache wiring in pagination pipeline"
|
||||
contains: "RDEPUBTextRendererCache.shared"
|
||||
- path: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift"
|
||||
provides: "Disk-persistent RDEPUBTextBook cache with SHA256 key generation"
|
||||
contains: "class RDEPUBTextBookCache"
|
||||
min_lines: 100
|
||||
key_links:
|
||||
- from: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift"
|
||||
to: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift"
|
||||
via: "cache lookup before rd_paginatedFrames, store after"
|
||||
pattern: "RDEPUBTextRendererCache\\.shared"
|
||||
- from: "Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift"
|
||||
to: "RDEPUBTextLayoutFrame"
|
||||
via: "cached value type"
|
||||
pattern: "\\[RDEPUBTextLayoutFrame\\]"
|
||||
- from: "RDEPUBTextBookCache.swift"
|
||||
to: "Library/Caches/RDEPUBTextBookCache/"
|
||||
via: "FileManager.default.urls(for: .cachesDirectory)"
|
||||
pattern: "cachesDirectory"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the pagination cache infrastructure and wire it into the rendering pipeline so that repeated pagination of the same chapter with unchanged viewport and typography configuration returns cached layout frames without re-typesetting.
|
||||
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: Prevents the same chapter from being fully re-typeset when viewport and typography config haven't changed (QUAL-01). The cache wraps the `rd_paginatedFrames` call in `RDEPUBTextBookBuilder`, which is the actual pagination bottleneck.
|
||||
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: `RDEPUBTextRendererCache.swift` with cache key struct and manager, plus updated `RDEPUBTextBookBuilder.swift` with cache wiring.
|
||||
Output: A new `RDEPUBTextBookCache.swift` file providing load/save/invalidate operations.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -52,153 +47,189 @@ Output: `RDEPUBTextRendererCache.swift` with cache key struct and manager, plus
|
||||
@.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>
|
||||
<!-- Existing types the executor needs to know about -->
|
||||
<!-- Key types the executor must serialize. From RDEPUBTextBookBuilder.swift lines 16-94 -->
|
||||
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift:
|
||||
- `RDEPUBTextChapterRenderRequest` — contains `context: RDEPUBTextChapterContext`, `style: RDEPUBTextRenderStyle`
|
||||
- `RDEPUBTextChapterContext` — has `href: String`, `title: String`, `html: String`, `baseURL: URL?`
|
||||
- `RDEPUBTextRenderStyle` — has `font: UIFont`, `lineSpacing: CGFloat`, `textColor: UIColor?`, `backgroundColor: UIColor?`
|
||||
- `RDEPUBRenderedChapterContent` — has `attributedString: NSAttributedString`, `fragmentOffsets: [String: Int]`
|
||||
From RDEPUBTextBookBuilder.swift:
|
||||
```swift
|
||||
public struct RDEPUBTextBook: Equatable {
|
||||
public var chapters: [RDEPUBTextChapter]
|
||||
public var pages: [RDEPUBTextPage]
|
||||
public init(chapters: [RDEPUBTextChapter], pages: [RDEPUBTextPage])
|
||||
}
|
||||
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift:
|
||||
- `struct RDEPUBTextLayoutFrame: Equatable` — value type with contentRange, breakReason, attachmentRanges, diagnostics, etc.
|
||||
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]
|
||||
}
|
||||
|
||||
From Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (line 135, 196):
|
||||
- `buildBook(... pageSize: CGSize, ...)` — the function that iterates spine items
|
||||
- `content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)` — the pagination call to cache around
|
||||
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 Sources/RDReaderView/EPUBTextRendering/RDEPUBTextPaginationSupport.swift:
|
||||
- `rd_paginatedFrames(size: CGSize, fragmentOffsets: [String: Int]) -> [RDEPUBTextLayoutFrame]` — extension on NSAttributedString
|
||||
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 cache key struct and cache manager</name>
|
||||
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift</files>
|
||||
<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/RDEPUBTextRenderer.swift (understand RDEPUBTextRenderStyle, RDEPUBTextChapterRenderRequest, RDEPUBTextChapterContext)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayoutFrame.swift (understand the value type to cache)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (lines 130-200 — understand pageSize and rd_paginatedFrames usage)
|
||||
Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
|
||||
Sources/RDReaderView/EPUBCore/RDEPUBReadingModels.swift
|
||||
Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift
|
||||
</read_first>
|
||||
<action>
|
||||
Create `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift` with:
|
||||
Create a new file `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift` implementing the complete cache system.
|
||||
|
||||
1. `RDEPUBPaginationCacheKey` struct — captures the inputs that determine pagination output:
|
||||
- `chapterID: String` (the chapter href from `RDEPUBTextChapterContext.href`)
|
||||
- `viewportSize: CGSize` (the `pageSize` passed to `buildBook`)
|
||||
- `fontDescriptor: String` (font family + name + pointSize, concatenated, e.g. `"\(font.familyName)-\(font.fontName)-\(font.pointSize)"`)
|
||||
- `lineSpacing: CGFloat`
|
||||
- `textColorHash: Int` (hash of textColor, or 0 if nil)
|
||||
- Implement `Equatable` conformance (auto-synthesize is fine since all stored properties are Equatable)
|
||||
**RDEPUBTextBookCache class** (per D-01):
|
||||
|
||||
2. `RDEPUBTextRendererCache` class:
|
||||
- `static let shared = RDEPUBTextRendererCache()`
|
||||
- `private var cache: [RDEPUBPaginationCacheKey: [RDEPUBTextLayoutFrame]]` — maps key to array of layout frames
|
||||
- `private let lock = NSLock()` — thread safety
|
||||
- `func cachedFrames(for key: RDEPUBPaginationCacheKey) -> [RDEPUBTextLayoutFrame]?` — lock, lookup, unlock, return
|
||||
- `func store(frames: [RDEPUBTextLayoutFrame], for key: RDEPUBPaginationCacheKey)` — lock, store, unlock
|
||||
- `func invalidate(for key: RDEPUBPaginationCacheKey)` — lock, remove, unlock
|
||||
- `func invalidateAll()` — lock, removeAll, unlock
|
||||
- `var entryCount: Int` — lock, read count, unlock (for diagnostics)
|
||||
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
|
||||
|
||||
Make the class `final`. Use `NSLock` for thread safety since the cache may be accessed from different queues.
|
||||
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 -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
|
||||
<automated>xcodebuild build -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 17' 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift` exists
|
||||
- File contains `struct RDEPUBPaginationCacheKey: Equatable` with properties: chapterID, viewportSize, fontDescriptor, lineSpacing, textColorHash
|
||||
- File contains `final class RDEPUBTextRendererCache` with static `shared` instance
|
||||
- Cache value type is `[RDEPUBTextLayoutFrame]` (not `[NSAttributedString]`)
|
||||
- Class has methods: `cachedFrames(for:)`, `store(frames:for:)`, `invalidate(for:)`, `invalidateAll()`
|
||||
- Class has computed property `entryCount: Int`
|
||||
- Build succeeds (xcodebuild exits 0)
|
||||
- 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>Cache key struct and cache manager exist with correct value types, build compiles successfully</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Wire cache into pagination pipeline in RDEPUBTextBookBuilder</name>
|
||||
<files>Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift</files>
|
||||
<read_first>
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift (full file — understand buildBook function, pageSize parameter, rd_paginatedFrames call at line 196)
|
||||
- Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererCache.swift (created in Task 1 — understand API surface)
|
||||
</read_first>
|
||||
<action>
|
||||
Wire the cache into `RDEPUBTextBookBuilder.swift` around the `rd_paginatedFrames` call at line 196.
|
||||
|
||||
**Location:** Inside the `buildBook` function, around the existing pagination block (lines 194-198):
|
||||
```swift
|
||||
layoutFrames = content.length > 0
|
||||
? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
|
||||
: []
|
||||
```
|
||||
|
||||
**Replace with cache-aware logic:**
|
||||
|
||||
1. Before the pagination call, build a cache key:
|
||||
```swift
|
||||
let cacheKey = RDEPUBPaginationCacheKey(
|
||||
chapterID: item.href,
|
||||
viewportSize: pageSize,
|
||||
fontDescriptor: "\(style.font.familyName)-\(style.font.fontName)-\(style.font.pointSize)",
|
||||
lineSpacing: style.lineSpacing,
|
||||
textColorHash: style.textColor?.hashValue ?? 0
|
||||
)
|
||||
```
|
||||
|
||||
2. Check cache first:
|
||||
```swift
|
||||
if let cached = RDEPUBTextRendererCache.shared.cachedFrames(for: cacheKey) {
|
||||
layoutFrames = cached
|
||||
} else {
|
||||
layoutFrames = content.length > 0
|
||||
? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
|
||||
: []
|
||||
if !layoutFrames.isEmpty {
|
||||
RDEPUBTextRendererCache.shared.store(frames: layoutFrames, for: cacheKey)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Keep the cover chapter special-case path (lines 175-193) unchanged — it doesn't go through `rd_paginatedFrames`.
|
||||
|
||||
The `style` variable is available in scope (it's the `style: RDEPUBTextRenderStyle` parameter of `buildBook`). The `item.href` is the chapter identifier from the spine iteration.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>xcodebuild build -project ReadViewDemo/ReadViewDemo.xcodeproj -scheme ReadViewDemo -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- RDEPUBTextBookBuilder.swift imports or references `RDEPUBTextRendererCache`
|
||||
- Cache key is constructed from `item.href`, `pageSize`, `style.font`, `style.lineSpacing`, `style.textColor`
|
||||
- `RDEPUBTextRendererCache.shared.cachedFrames(for:)` is called before `rd_paginatedFrames`
|
||||
- `RDEPUBTextRendererCache.shared.store(frames:for:)` is called after successful pagination
|
||||
- Cover chapter special-case path is NOT modified
|
||||
- Build succeeds (xcodebuild exits 0)
|
||||
</acceptance_criteria>
|
||||
<done>Cache is wired into the pagination pipeline — repeated pagination of the same chapter with identical config returns cached frames, build compiles</done>
|
||||
<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>
|
||||
- Build succeeds with both new and modified files
|
||||
- Cache types are properly defined and accessible from RDEPUBTextBookBuilder
|
||||
- Cache key captures all inputs that affect pagination output (chapterID, viewportSize, font, lineSpacing, textColor)
|
||||
- Cache value is [RDEPUBTextLayoutFrame] matching the rd_paginatedFrames return type
|
||||
- `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>
|
||||
- RDEPUBPaginationCacheKey captures all inputs that affect pagination output
|
||||
- RDEPUBTextRendererCache provides a thread-safe, singleton cache surface
|
||||
- Cache is wired into the actual pagination path in RDEPUBTextBookBuilder
|
||||
- Same chapter + same config = cache hit (no re-typesetting)
|
||||
- Different viewport or typography = cache miss (fresh pagination)
|
||||
- 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>
|
||||
|
||||
Reference in New Issue
Block a user