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-09-10 03:46:58 +02:00
|
|
|
typealias StatusesCompletionBlock = ([String: ArticleStatus]) -> Void // [articleID: Status]
|
|
|
|
|
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-09-05 17:53:45 +02:00
|
|
|
private let queue: RSDatabaseQueue
|
|
|
|
|
|
|
|
init(queue: RSDatabaseQueue) {
|
|
|
|
|
|
|
|
self.queue = queue
|
|
|
|
}
|
|
|
|
|
|
|
|
func cachedStatus(for articleID: String) -> ArticleStatus? {
|
2017-07-29 21:13:38 +02:00
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
assert(Thread.isMainThread)
|
|
|
|
assert(cache[articleID] != nil)
|
2017-09-05 03:29:02 +02:00
|
|
|
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
|
|
|
|
2017-09-10 03:46:58 +02:00
|
|
|
func ensureStatusesForArticleIDs(_ articleIDs: Set<String>, _ completion: @escaping StatusesCompletionBlock) {
|
2017-09-05 17:53:45 +02:00
|
|
|
|
|
|
|
// Adds them to the cache if not cached.
|
|
|
|
|
|
|
|
assert(Thread.isMainThread)
|
|
|
|
|
2017-09-03 01:08:02 +02:00
|
|
|
// Check cache.
|
|
|
|
let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs)
|
|
|
|
if articleIDsMissingCachedStatus.isEmpty {
|
2017-09-10 03:46:58 +02:00
|
|
|
completion(statusesDictionary(articleIDs))
|
2017-09-03 01:08:02 +02:00
|
|
|
return
|
|
|
|
}
|
2017-09-05 17:53:45 +02:00
|
|
|
|
2017-09-03 01:08:02 +02:00
|
|
|
// Check database.
|
2017-09-05 17:53:45 +02:00
|
|
|
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus) {
|
|
|
|
|
2017-09-10 03:46:58 +02:00
|
|
|
let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs)
|
2017-09-14 06:41:01 +02:00
|
|
|
if !articleIDsNeedingStatus.isEmpty {
|
|
|
|
// Create new statuses.
|
|
|
|
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus)
|
2017-09-05 17:53:45 +02:00
|
|
|
}
|
|
|
|
|
2017-09-14 06:41:01 +02:00
|
|
|
completion(self.statusesDictionary(articleIDs))
|
2017-09-03 01:08:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-05 03:29:02 +02:00
|
|
|
// MARK: Marking
|
|
|
|
|
|
|
|
func markArticleIDs(_ articleIDs: Set<String>, _ statusKey: String, _ flag: Bool, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
return articleStatus
|
2017-07-03 19:40:48 +02:00
|
|
|
}
|
2017-08-01 03:39:42 +02:00
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
// MARK: Cache
|
|
|
|
|
2017-09-03 01:08:02 +02:00
|
|
|
func articleIDsWithNoCachedStatus(_ articleIDs: Set<String>) -> Set<String> {
|
|
|
|
|
2017-09-10 03:46:58 +02:00
|
|
|
assert(Thread.isMainThread)
|
2017-09-03 01:08:02 +02:00
|
|
|
return Set(articleIDs.filter { cache[$0] == nil })
|
|
|
|
}
|
|
|
|
|
2017-09-10 03:46:58 +02:00
|
|
|
func statusesDictionary(_ articleIDs: Set<String>) -> [String: ArticleStatus] {
|
|
|
|
|
|
|
|
assert(Thread.isMainThread)
|
|
|
|
|
|
|
|
var d = [String: ArticleStatus]()
|
|
|
|
|
|
|
|
for articleID in articleIDs {
|
|
|
|
if let articleStatus = cache[articleID] {
|
|
|
|
d[articleID] = articleStatus
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
2017-07-03 19:40:48 +02:00
|
|
|
// MARK: Creating
|
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
func saveStatuses(_ statuses: Set<ArticleStatus>) {
|
2017-08-27 00:37:15 +02:00
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
queue.update { (database) in
|
2017-09-14 06:41:01 +02:00
|
|
|
let statusArray = statuses.map { $0.databaseDictionary()! }
|
2017-09-10 03:46:58 +02:00
|
|
|
self.insertRows(statusArray, insertType: .orIgnore, in: database)
|
2017-09-05 17:53:45 +02:00
|
|
|
}
|
2017-08-27 00:37:15 +02:00
|
|
|
}
|
|
|
|
|
2017-09-14 06:41:01 +02:00
|
|
|
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>) {
|
2017-08-27 00:37:15 +02:00
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
assert(Thread.isMainThread)
|
|
|
|
|
2017-08-27 00:37:15 +02:00
|
|
|
let now = Date()
|
2017-09-03 01:08:02 +02:00
|
|
|
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) })
|
2017-09-05 17:53:45 +02:00
|
|
|
cache.addIfNotCached(statuses)
|
|
|
|
|
|
|
|
saveStatuses(statuses)
|
2017-08-27 00:37:15 +02:00
|
|
|
}
|
2017-07-03 19:40:48 +02:00
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set<String>, _ completion: @escaping RSVoidCompletionBlock) {
|
|
|
|
|
|
|
|
queue.fetch { (database) in
|
2017-09-10 03:46:58 +02:00
|
|
|
guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else {
|
2017-09-05 17:53:45 +02:00
|
|
|
completion()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-09-10 03:46:58 +02:00
|
|
|
let statuses = resultSet.mapToSet(self.statusWithRow)
|
2017-09-05 17:53:45 +02:00
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
2017-09-10 03:46:58 +02:00
|
|
|
self.cache.addIfNotCached(statuses)
|
2017-09-05 17:53:45 +02:00
|
|
|
completion()
|
|
|
|
}
|
2017-09-03 01:08:02 +02:00
|
|
|
}
|
2017-09-05 03:29:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private final class StatusCache {
|
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
// Main thread only.
|
2017-09-05 03:29:02 +02:00
|
|
|
|
|
|
|
var dictionary = [String: ArticleStatus]()
|
|
|
|
|
|
|
|
func add(_ statuses: Set<ArticleStatus>) {
|
|
|
|
|
|
|
|
// Replaces any cached statuses.
|
|
|
|
|
|
|
|
for status in statuses {
|
|
|
|
self[status.articleID] = status
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-05 17:53:45 +02:00
|
|
|
func addIfNotCached(_ statuses: Set<ArticleStatus>) {
|
|
|
|
|
|
|
|
// Does not replace already cached statuses.
|
|
|
|
|
|
|
|
for status in statuses {
|
|
|
|
let articleID = status.articleID
|
|
|
|
if let _ = self[articleID] {
|
|
|
|
continue
|
2017-09-05 03:29:02 +02:00
|
|
|
}
|
2017-09-05 17:53:45 +02:00
|
|
|
self[articleID] = status
|
2017-09-05 03:29:02 +02:00
|
|
|
}
|
|
|
|
}
|
2017-09-05 17:53:45 +02:00
|
|
|
|
2017-09-09 21:57:24 +02:00
|
|
|
subscript(_ articleID: String) -> ArticleStatus? {
|
2017-09-05 03:29:02 +02:00
|
|
|
get {
|
|
|
|
return self[articleID]
|
|
|
|
}
|
|
|
|
set {
|
|
|
|
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
|
|
|
|
|
|
|
|