2017-07-03 20:20:14 +02:00
|
|
|
//
|
2017-07-29 21:08:10 +02:00
|
|
|
// StatusesTable.swift
|
2017-07-03 19:40:48 +02:00
|
|
|
// Evergreen
|
|
|
|
//
|
|
|
|
// Created by Brent Simmons on 5/8/16.
|
|
|
|
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import RSCore
|
|
|
|
import RSDatabase
|
|
|
|
import Data
|
|
|
|
|
2017-08-06 21:37:47 +02:00
|
|
|
// Article->ArticleStatus is a to-one relationship.
|
|
|
|
//
|
|
|
|
// CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0, accountInfo BLOB);
|
|
|
|
|
2017-07-29 21:08:10 +02:00
|
|
|
final class StatusesTable: DatabaseTable {
|
2017-07-30 20:22:21 +02:00
|
|
|
|
2017-09-02 23:19:42 +02:00
|
|
|
let name = DatabaseTableName.statuses
|
2017-09-05 03:29:02 +02:00
|
|
|
private let cache = StatusCache()
|
2017-07-29 21:13:38 +02:00
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
func existingStatus(for articleID: String) -> ArticleStatus? {
|
2017-08-27 00:37:15 +02:00
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
cache.lock()
|
|
|
|
defer { cache.unlock() }
|
|
|
|
return cache[articleID]
|
2017-08-27 00:37:15 +02:00
|
|
|
}
|
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
// MARK: Creating/Updating
|
2017-09-03 01:08:02 +02:00
|
|
|
|
|
|
|
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
cache.lock()
|
|
|
|
defer { cache.unlock() }
|
|
|
|
|
2017-09-03 01:08:02 +02:00
|
|
|
// Check cache.
|
|
|
|
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
|
|
|
if articleIDsMissingCachedStatus.isEmpty {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check database.
|
|
|
|
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
|
|
|
|
let articleIDsNeedingStatus = articleIDsWithNoCachedStatus(articleIDs)
|
|
|
|
if articleIDsNeedingStatus.isEmpty {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create new statuses.
|
|
|
|
createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, database)
|
|
|
|
}
|
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
// MARK: Marking
|
|
|
|
|
|
|
|
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: String, _ flag: Bool, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
cache.lock()
|
|
|
|
defer { cache.unlock() }
|
|
|
|
|
|
|
|
// TODO: replace statuses in cache.
|
|
|
|
|
|
|
|
updateRowsWithValue(NSNumber(value: flag), valueKey: statusKey, whereKey: DatabaseKey.articleID, matches: Array(articleIDs), database: database)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
private extension StatusesTable {
|
|
|
|
|
|
|
|
// MARK: Fetching
|
|
|
|
|
2017-09-05 02:10:02 +02:00
|
|
|
func fetchStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) -> [String: ArticleStatus] {
|
|
|
|
|
|
|
|
// Does not create statuses. Checks cache first, then database only if needed.
|
|
|
|
|
|
|
|
var d = [String: ArticleStatus]()
|
|
|
|
var articleIDsMissingCachedStatus = Set<String>()
|
|
|
|
|
|
|
|
for articleID in articleIDs {
|
|
|
|
if let cachedStatus = cache[articleID] as? ArticleStatus {
|
|
|
|
d[articleID] = cachedStatus
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
articleIDsMissingCachedStatus.insert(articleID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if articleIDsMissingCachedStatus.isEmpty {
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
|
|
|
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus, database)
|
|
|
|
for articleID in articleIDsMissingCachedStatus {
|
|
|
|
if let cachedStatus = cache[articleID] as? ArticleStatus {
|
|
|
|
d[articleID] = cachedStatus
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
func statusWithRow(_ row: FMResultSet) -> ArticleStatus? {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
guard let articleID = row.string(forColumn: DatabaseKey.articleID) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if let cachedStatus = cache[articleID] as? ArticleStatus {
|
|
|
|
return cachedStatus
|
|
|
|
}
|
2017-08-27 00:37:15 +02:00
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
guard let dateArrived = row.date(forColumn: DatabaseKey.dateArrived) else {
|
|
|
|
return nil
|
2017-08-06 21:37:47 +02:00
|
|
|
}
|
2017-09-05 03:29:02 +02:00
|
|
|
|
|
|
|
let articleStatus = ArticleStatus(articleID: articleID, dateArrived: dateArrived, row: row)
|
|
|
|
cache[articleID] = articleStatus
|
|
|
|
return articleStatus
|
2017-07-03 19:40:48 +02:00
|
|
|
}
|
2017-08-01 03:39:42 +02:00
|
|
|
|
2017-09-03 01:08:02 +02:00
|
|
|
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
|
|
|
|
|
|
|
|
return Set(articleIDs.filter { cache[$0] == nil })
|
|
|
|
}
|
|
|
|
|
2017-07-03 19:40:48 +02:00
|
|
|
// MARK: Creating
|
|
|
|
|
2017-08-27 00:37:15 +02:00
|
|
|
func saveStatuses(_ statuses: Set<ArticleStatus>, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
let statusArray = statuses.map { $0.databaseDictionary() }
|
|
|
|
insertRows(statusArray, insertType: .orIgnore, in: database)
|
|
|
|
}
|
|
|
|
|
|
|
|
func createAndSaveStatusesForArticles(_ articles: Set<Article>, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
let articleIDs = Set(articles.map { $0.articleID })
|
|
|
|
createAndSaveStatusesForArticleIDs(articleIDs, database)
|
|
|
|
}
|
|
|
|
|
|
|
|
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
let now = Date()
|
2017-09-03 01:08:02 +02:00
|
|
|
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) })
|
2017-08-27 00:37:15 +02:00
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
cache.add(statuses)
|
|
|
|
|
2017-09-03 01:08:02 +02:00
|
|
|
saveStatuses(statuses, database)
|
2017-08-27 00:37:15 +02:00
|
|
|
}
|
2017-07-03 19:40:48 +02:00
|
|
|
|
2017-09-03 01:08:02 +02:00
|
|
|
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
guard let resultSet = selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let statuses = resultSet.mapToSet(statusWithRow)
|
2017-09-05 03:29:02 +02:00
|
|
|
cache.add(statuses)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private final class StatusCache {
|
|
|
|
|
|
|
|
// Locking is left to the caller. Use the provided lock methods.
|
|
|
|
|
|
|
|
private let lock = NSLock()
|
|
|
|
private var isLocked = false
|
|
|
|
var dictionary = [String: ArticleStatus]()
|
|
|
|
|
|
|
|
func lock() {
|
|
|
|
|
|
|
|
assert(!isLocked)
|
|
|
|
lock.lock()
|
|
|
|
isLocked = true
|
|
|
|
}
|
|
|
|
|
|
|
|
func unlock() {
|
|
|
|
|
|
|
|
assert(isLocked)
|
|
|
|
lock.unlock()
|
|
|
|
isLocked = false
|
|
|
|
}
|
|
|
|
|
|
|
|
func add(_ statuses: Set<ArticleStatus>) {
|
|
|
|
|
|
|
|
// Replaces any cached statuses.
|
|
|
|
|
|
|
|
assert(isLocked)
|
|
|
|
for status in statuses {
|
|
|
|
self[status.articleID] = status
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func statuses(for articleIDs: Set<String>) -> [String: ArticleStatus] {
|
|
|
|
|
|
|
|
assert(isLocked)
|
|
|
|
|
|
|
|
var d = [String: ArticleStatus]()
|
|
|
|
for articleID in articleIDs {
|
|
|
|
if let cachedStatus = self[articleID] {
|
|
|
|
d[articleID] = cachedStatus
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
|
|
|
subscript(_ articleID: String) -> ArticleStatus {
|
|
|
|
get {
|
|
|
|
assert(isLocked)
|
|
|
|
return self[articleID]
|
|
|
|
}
|
|
|
|
set {
|
|
|
|
assert(isLocked)
|
|
|
|
self[articleID] = newValue
|
|
|
|
}
|
2017-09-03 01:08:02 +02:00
|
|
|
}
|
2017-07-03 19:40:48 +02:00
|
|
|
}
|
2017-09-05 03:29:02 +02:00
|
|
|
|
|
|
|
|