Continue transition from ODB to FeedMetadata.plist. It’s simpler and uses less memory.

This commit is contained in:
Brent Simmons 2019-03-13 23:41:43 -07:00
parent 57c5854efa
commit 79a6d5f761
3 changed files with 102 additions and 23 deletions

View File

@ -80,6 +80,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
private var _flattenedFeeds = Set<Feed>() private var _flattenedFeeds = Set<Feed>()
private var flattenedFeedsNeedUpdate = true private var flattenedFeedsNeedUpdate = true
private let feedMetadataPath: String
private typealias FeedMetadataDictionary = [String: FeedMetadata]
private var feedMetadata = FeedMetadataDictionary()
private var feedMetadataDirty = false {
didSet {
queueSaveMetadataIfNeeded()
}
}
private var startingUp = true private var startingUp = true
private struct SettingsKey { private struct SettingsKey {
@ -137,6 +146,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3") let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3")
self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID) self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID)
self.feedMetadataPath = (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist")
let settingsODBFilePath = (dataFolder as NSString).appendingPathComponent("Settings.odb") let settingsODBFilePath = (dataFolder as NSString).appendingPathComponent("Settings.odb")
self.settingsODB = ODB(filepath: settingsODBFilePath) self.settingsODB = ODB(filepath: settingsODBFilePath)
self.settingsODB.vacuum() self.settingsODB.vacuum()
@ -167,6 +177,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
delegate.refreshAll(for: self) delegate.refreshAll(for: self)
} }
func metadata(for feed: Feed) -> FeedMetadata {
if let d = feedMetadata[feed.feedID] {
assert(d.delegate === self)
return d
}
let d = FeedMetadata(feedID: feed.feedID)
d.delegate = self
feedMetadata[feed.feedID] = d
return d
}
public func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) { public func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) {
feed.takeSettings(from: parsedFeed) feed.takeSettings(from: parsedFeed)
@ -559,6 +580,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
} }
} }
@objc func saveMetadataIfNeeded() {
if feedMetadataDirty {
saveFeedMetadata()
}
}
// MARK: - Hashable // MARK: - Hashable
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
@ -590,6 +617,15 @@ extension Account {
} }
} }
// MARK: - FeedMetadataDelegate
extension Account: FeedMetadataDelegate {
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
feedMetadataDirty = true
}
}
// MARK: - Disk (Private) // MARK: - Disk (Private)
private extension Account { private extension Account {
@ -614,7 +650,7 @@ private extension Account {
} }
func pullObjectsFromDisk() { func pullObjectsFromDisk() {
// 9/16/2018: Turning a corner  we used to store data in a plist file, // 9/16/2018: Turning a corner  we used to store data in a plist file,
// but now were switching over to OPML. Read the plist file one last time, // but now were switching over to OPML. Read the plist file one last time,
// then rename it so we never read from it again. // then rename it so we never read from it again.
@ -654,9 +690,20 @@ private extension Account {
return return
} }
importFeedMetadata()
importOPMLFile(path: opmlFilePath) importOPMLFile(path: opmlFilePath)
} }
func importFeedMetadata() {
let url = URL(fileURLWithPath: feedMetadataPath)
guard let data = try? Data(contentsOf: url) else {
return
}
let decoder = PropertyListDecoder()
feedMetadata = (try? decoder.decode(FeedMetadataDictionary.self, from: data)) ?? FeedMetadataDictionary()
feedMetadata.values.forEach { $0.delegate = self }
}
func importOPMLFile(path: String) { func importOPMLFile(path: String) {
let opmlFileURL = URL(fileURLWithPath: path) let opmlFileURL = URL(fileURLWithPath: path)
var fileData: Data? var fileData: Data?
@ -703,6 +750,33 @@ private extension Account {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
} }
func queueSaveMetadataIfNeeded() {
Account.saveQueue.add(self, #selector(saveMetadataIfNeeded))
}
private func metadataForOnlySubscribedToFeeds() -> FeedMetadataDictionary {
let feedIDs = idToFeedDictionary.keys
return feedMetadata.filter { (feedID: String, metadata: FeedMetadata) -> Bool in
return feedIDs.contains(feedID)
}
}
func saveFeedMetadata() {
feedMetadataDirty = false
let d = metadataForOnlySubscribedToFeeds()
let encoder = PropertyListEncoder()
encoder.outputFormat = .binary
let url = URL(fileURLWithPath: feedMetadataPath)
do {
let data = try encoder.encode(d)
try data.write(to: url)
}
catch {
assertionFailure(error.localizedDescription)
}
}
} }
// MARK: - Private // MARK: - Private

View File

@ -35,33 +35,33 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha
public var homePageURL: String? { public var homePageURL: String? {
get { get {
return settingsTable.string(for: Key.homePageURL) return metadata?.homePageURL
} }
set { set {
if let url = newValue { if let url = newValue {
settingsTable.setString(url.rs_normalizedURL(), for: Key.homePageURL) metadata?.homePageURL = url.rs_normalizedURL()
} }
else { else {
settingsTable.setString(nil, for: Key.homePageURL) metadata?.homePageURL = nil
} }
} }
} }
public var iconURL: String? { public var iconURL: String? {
get { get {
return settingsTable.string(for: Key.iconURL) return metadata?.iconURL
} }
set { set {
settingsTable.setString(newValue, for: Key.iconURL) metadata?.iconURL = newValue
} }
} }
public var faviconURL: String? { public var faviconURL: String? {
get { get {
return settingsTable.string(for: Key.faviconURL) return metadata?.faviconURL
} }
set { set {
settingsTable.setString(newValue, for: Key.faviconURL) metadata?.faviconURL = newValue
} }
} }
@ -80,17 +80,17 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha
public var authors: Set<Author>? { public var authors: Set<Author>? {
get { get {
guard let authorsJSON = settingsTable.string(for: Key.authors) else { if let authorsArray = metadata?.authors {
return nil return Set(authorsArray)
} }
return Author.authorsWithJSON(authorsJSON) return nil
} }
set { set {
if let authorsJSON = newValue?.json() { if let authorsSet = newValue {
settingsTable.setString(authorsJSON, for: Key.authors) metadata?.authors = Array(authorsSet)
} }
else { else {
settingsTable.setString(nil, for: Key.authors) metadata?.authors = nil
} }
} }
} }
@ -118,22 +118,19 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha
public var conditionalGetInfo: HTTPConditionalGetInfo? { public var conditionalGetInfo: HTTPConditionalGetInfo? {
get { get {
let lastModified = settingsTable.string(for: Key.conditionalGetLastModified) return metadata?.conditionalGetInfo
let etag = settingsTable.string(for: Key.conditionalGetEtag)
return HTTPConditionalGetInfo(lastModified: lastModified, etag: etag)
} }
set { set {
settingsTable.setString(newValue?.lastModified, for: Key.conditionalGetLastModified) metadata?.conditionalGetInfo = newValue
settingsTable.setString(newValue?.etag, for: Key.conditionalGetEtag)
} }
} }
public var contentHash: String? { public var contentHash: String? {
get { get {
return settingsTable.string(for: Key.contentHash) return metadata?.contentHash
} }
set { set {
settingsTable.setString(newValue, for: Key.contentHash) metadata?.contentHash = newValue
} }
} }
@ -173,6 +170,15 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha
private let settingsTable: ODBRawValueTable private let settingsTable: ODBRawValueTable
private let accountID: String // Used for hashing and equality; account may turn nil private let accountID: String // Used for hashing and equality; account may turn nil
private var _metadata: FeedMetadata?
private var metadata: FeedMetadata? {
if let cachedMetadata = _metadata {
return cachedMetadata
}
_metadata = account?.metadata(for: self)
return _metadata
}
// MARK: - Init // MARK: - Init
public init(account: Account, url: String, feedID: String) { public init(account: Account, url: String, feedID: String) {

View File

@ -96,9 +96,8 @@ final class FeedMetadata: Codable {
weak var delegate: FeedMetadataDelegate? weak var delegate: FeedMetadataDelegate?
init(feedID: String, delegate: FeedMetadataDelegate) { init(feedID: String) {
self.feedID = feedID self.feedID = feedID
self.delegate = delegate
} }
func valueDidChange(_ key: CodingKeys) { func valueDidChange(_ key: CodingKeys) {