Continue transition from ODB to FeedMetadata.plist. It’s simpler and uses less memory.
This commit is contained in:
parent
57c5854efa
commit
79a6d5f761
|
@ -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 we’re switching over to OPML. Read the plist file one last time,
|
// but now we’re 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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue