extract sync logic from service hooks

This commit is contained in:
刘浩远 2020-08-14 17:02:19 +08:00
parent c52b3bf00c
commit 2676be240f
2 changed files with 175 additions and 144 deletions

View File

@ -1,16 +1,19 @@
import * as db from "../db"
import { SyncService, ServiceConfigs } from "../../schema-types"
import { AppThunk, ActionStatus } from "../utils"
import { RSSItem } from "./item"
import { AppThunk, ActionStatus, fetchFavicon } from "../utils"
import { RSSItem, insertItems, fetchItemsSuccess } from "./item"
import { feverServiceHooks } from "./services/fever"
import { saveSettings } from "./app"
import { deleteSource } from "./source"
import { saveSettings, pushNotification } from "./app"
import { deleteSource, updateUnreadCounts, RSSSource, insertSource, addSourceSuccess, updateSource } from "./source"
import { FilterType, initFeeds } from "./feed"
import { createSourceGroup, addSourceToGroup } from "./group"
export interface ServiceHooks {
authenticate?: (configs: ServiceConfigs) => Promise<boolean>
updateSources?: () => AppThunk<Promise<void>>
fetchItems?: (background: boolean) => AppThunk<Promise<void>>
syncItems?: () => AppThunk<Promise<void>>
updateSources?: () => AppThunk<Promise<[RSSSource[], Map<number | string, string>]>>
fetchItems?: () => AppThunk<Promise<[RSSItem[], ServiceConfigs]>>
syncItems?: () => AppThunk<Promise<[(number | string)[], (number | string)[]]>>
markRead?: (item: RSSItem) => AppThunk
markUnread?: (item: RSSItem) => AppThunk
markAllRead?: (sids?: number[], date?: Date, before?: boolean) => AppThunk
@ -40,9 +43,9 @@ export function syncWithService(background = false): AppThunk<Promise<void>> {
type: SYNC_SERVICE,
status: ActionStatus.Request
})
await dispatch(hooks.updateSources())
await dispatch(hooks.syncItems())
await dispatch(hooks.fetchItems(background))
await dispatch(updateSources(hooks.updateSources))
await dispatch(syncItems(hooks.syncItems))
await dispatch(fetchItems(hooks.fetchItems, background))
dispatch({
type: SYNC_SERVICE,
status: ActionStatus.Success
@ -61,6 +64,139 @@ export function syncWithService(background = false): AppThunk<Promise<void>> {
}
}
function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<void>> {
return async (dispatch, getState) => {
const [sources, groupsMap] = await dispatch(hook())
const existing = new Map<number | string, RSSSource>()
for (let source of Object.values(getState().sources)) {
if (source.serviceRef) {
existing.set(source.serviceRef, source)
}
}
const forceSettings = () => {
if (!(getState().app.settings.saving)) dispatch(saveSettings())
}
let promises = sources.map(s => new Promise<RSSSource>((resolve, reject) => {
if (existing.has(s.serviceRef)) {
const doc = existing.get(s.serviceRef)
existing.delete(s.serviceRef)
resolve(doc)
} else {
db.sdb.findOne({ url: s.url }, (err, doc) => {
if (err) {
reject(err)
} else if (doc === null) {
// Create a new source
forceSettings()
const domain = s.url.split("/").slice(0, 3).join("/")
fetchFavicon(domain).then(favicon => {
if (favicon) s.iconurl = favicon
dispatch(insertSource(s))
.then((inserted) => {
inserted.unreadCount = 0
resolve(inserted)
dispatch(addSourceSuccess(inserted, true))
})
.catch((err) => {
reject(err)
})
})
} else if (doc.serviceRef !== s.serviceRef) {
// Mark an existing source as remote and remove all items
forceSettings()
doc.serviceRef = s.serviceRef
doc.unreadCount = 0
dispatch(updateSource(doc)).finally(() => {
db.idb.remove({ source: doc.sid }, { multi: true }, (err) => {
if (err) reject(err)
else resolve(doc)
})
})
} else {
resolve(doc)
}
})
}
}))
for (let [_, source] of existing) {
// Delete sources removed from the service side
forceSettings()
promises.push(dispatch(deleteSource(source, true)).then(() => null))
}
let sourcesResults = (await Promise.all(promises)).filter(s => s)
if (groupsMap) {
// Add sources to imported groups
forceSettings()
for (let source of sourcesResults) {
if (groupsMap.has(source.serviceRef)) {
const gid = dispatch(createSourceGroup(groupsMap.get(source.serviceRef)))
dispatch(addSourceToGroup(gid, source.sid))
}
}
const configs = getState().service
delete configs.importGroups
dispatch(saveServiceConfigs(configs))
}
}
}
function syncItems(hook: ServiceHooks["syncItems"]): AppThunk<Promise<void>> {
return async (dispatch, getState) => {
const state = getState()
const [unreadRefs, starredRefs] = await dispatch(hook())
const promises = new Array<Promise<number>>()
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $in: unreadRefs },
hasRead: true
}, { $set: { hasRead: false } }, { multi: true }, (_, num) => resolve(num))
}))
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $nin: unreadRefs },
hasRead: false
}, { $set: { hasRead: true } }, { multi: true }, (_, num) => resolve(num))
}))
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $in: starredRefs },
starred: { $exists: false }
}, { $set: { starred: true } }, { multi: true }, (_, num) => resolve(num))
}))
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $nin: starredRefs },
starred: true
}, { $unset: { starred: true } }, { multi: true }, (_, num) => resolve(num))
}))
const affected = (await Promise.all(promises)).reduce((a, b) => a + b, 0)
if (affected > 0) {
dispatch(syncLocalItems(unreadRefs, starredRefs))
if (!(state.page.filter.type & FilterType.ShowRead) || !(state.page.filter.type & FilterType.ShowNotStarred)) {
dispatch(initFeeds(true))
}
await dispatch(updateUnreadCounts())
}
}
}
function fetchItems(hook: ServiceHooks["fetchItems"], background: boolean): AppThunk<Promise<void>> {
return async (dispatch, getState) => {
const [items, configs] = await dispatch(hook())
if (items.length > 0) {
const inserted = await insertItems(items)
dispatch(fetchItemsSuccess(inserted.reverse(), getState().items))
if (background) {
for (let item of inserted) {
if (item.notify) dispatch(pushNotification(item))
}
if (inserted.length > 0) window.utils.requestAttention()
}
dispatch(saveServiceConfigs(configs))
}
}
}
export function importGroups(): AppThunk<Promise<void>> {
return async (dispatch, getState) => {
const configs = getState().service
@ -103,8 +239,8 @@ interface SyncWithServiceAction {
interface SyncLocalItemsAction {
type: typeof SYNC_LOCAL_ITEMS
unreadIds: number[]
starredIds: number[]
unreadIds: (string | number)[]
starredIds: (string | number)[]
}
export type ServiceActionTypes = SaveServiceConfigsAction | SyncWithServiceAction | SyncLocalItemsAction
@ -119,7 +255,7 @@ export function saveServiceConfigs(configs: ServiceConfigs): AppThunk {
}
}
export function syncLocalItems(unread: number[], starred: number[]): ServiceActionTypes {
function syncLocalItems(unread: (string | number)[], starred: (string | number)[]): ServiceActionTypes {
return {
type: SYNC_LOCAL_ITEMS,
unreadIds: unread,

View File

@ -1,13 +1,10 @@
import intl from "react-intl-universal"
import * as db from "../../db"
import { ServiceHooks, saveServiceConfigs, syncLocalItems } from "../service"
import { ServiceHooks } from "../service"
import { ServiceConfigs, SyncService } from "../../../schema-types"
import { createSourceGroup, addSourceToGroup } from "../group"
import { RSSSource, insertSource, addSourceSuccess, updateSource, deleteSource, updateUnreadCounts } from "../source"
import { fetchFavicon, htmlDecode, domParser } from "../../utils"
import { saveSettings, pushNotification } from "../app"
import { initFeeds, FilterType } from "../feed"
import { RSSItem, insertItems, fetchItemsSuccess } from "../item"
import { createSourceGroup } from "../group"
import { RSSSource } from "../source"
import { htmlDecode, domParser } from "../../utils"
import { RSSItem } from "../item"
import { SourceRule } from "../rule"
export interface FeverConfigs extends ServiceConfigs {
@ -51,8 +48,7 @@ export const feverServiceHooks: ServiceHooks = {
},
updateSources: () => async (dispatch, getState) => {
const initState = getState()
const configs = initState.service as FeverConfigs
const configs = getState().service as FeverConfigs
const response = await fetchAPI(configs, "&feeds")
const feeds: any[] = response.feeds
const feedGroups: any[] = response.feeds_groups
@ -62,90 +58,28 @@ export const feverServiceHooks: ServiceHooks = {
// Import groups on the first sync
const groups: any[] = (await fetchAPI(configs, "&groups")).groups
if (groups === undefined || feedGroups === undefined) throw APIError()
groupsMap = new Map()
const groupsIdMap = new Map<number, string>()
for (let group of groups) {
dispatch(createSourceGroup(group.title))
groupsMap.set(group.id, group.title)
const title = group.title.trim()
dispatch(createSourceGroup(title))
groupsIdMap.set(group.id, title)
}
}
const existing = new Map<number, RSSSource>()
for (let source of Object.values(initState.sources)) {
if (source.serviceRef) {
existing.set(source.serviceRef as number, source)
}
}
const forceSettings = () => {
if (!(getState().app.settings.saving)) dispatch(saveSettings())
}
let promises = feeds.map(f => new Promise<RSSSource>((resolve, reject) => {
if (existing.has(f.id)) {
const doc = existing.get(f.id)
existing.delete(f.id)
resolve(doc)
} else {
db.sdb.findOne({ url: f.url }, (err, doc) => {
if (err) {
reject(err)
} else if (doc === null) {
// Create a new source
forceSettings()
let source = new RSSSource(f.url, f.title)
source.serviceRef = f.id
const domain = source.url.split("/").slice(0, 3).join("/")
fetchFavicon(domain).then(favicon => {
if (favicon) source.iconurl = favicon
dispatch(insertSource(source))
.then((inserted) => {
inserted.unreadCount = 0
resolve(inserted)
dispatch(addSourceSuccess(inserted, true))
})
.catch((err) => {
reject(err)
})
})
} else if (doc.serviceRef !== f.id) {
// Mark an existing source as remote and remove all items
forceSettings()
doc.serviceRef = f.id
doc.unreadCount = 0
dispatch(updateSource(doc)).finally(() => {
db.idb.remove({ source: doc.sid }, { multi: true }, (err) => {
if (err) reject(err)
else resolve(doc)
})
})
} else {
resolve(doc)
}
})
}
}))
for (let [_, source] of existing) {
// Delete sources removed from the service side
forceSettings()
promises.push(dispatch(deleteSource(source, true)).then(() => null))
}
let sources = (await Promise.all(promises)).filter(s => s)
if (groupsMap) {
// Add sources to imported groups
forceSettings()
let sourcesMap = new Map<number, number>()
for (let source of sources) sourcesMap.set(source.serviceRef as number, source.sid)
groupsMap = new Map()
for (let group of feedGroups) {
for (let fid of group.feed_ids.split(",").map(s => parseInt(s))) {
if (sourcesMap.has(fid)) {
const gid = dispatch(createSourceGroup(groupsMap.get(group.group_id)))
dispatch(addSourceToGroup(gid, sourcesMap.get(fid)))
}
groupsMap.set(fid, groupsIdMap.get(group.group_id))
}
}
delete configs.importGroups
dispatch(saveServiceConfigs(configs))
}
const sources = feeds.map(f => {
const source = new RSSSource(f.url, f.title)
source.serviceRef = f.id
return source
})
return [sources, groupsMap]
},
fetchItems: (background) => async (dispatch, getState) => {
fetchItems: () => async (_, getState) => {
const state = getState()
const configs = state.service as FeverConfigs
const items = new Array()
@ -199,7 +133,7 @@ export const feverServiceHooks: ServiceHooks = {
let img = dom.querySelector("img")
if (img && img.src) {
item.thumb = img.src
} else {
} else if (configs.useInt32) { // TTRSS Fever Plugin attachments
let a = dom.querySelector("body>ul>li:first-child>a") as HTMLAnchorElement
if (a && /, image\/generic$/.test(a.innerText) && a.href)
item.thumb = a.href
@ -212,21 +146,14 @@ export const feverServiceHooks: ServiceHooks = {
markItem(configs, item, item.starred ? "saved" : "unsaved")
return item
})
const inserted = await insertItems(parsedItems)
dispatch(fetchItemsSuccess(inserted.reverse(), getState().items))
if (background) {
for (let item of inserted) {
if (item.notify) dispatch(pushNotification(item))
}
if (inserted.length > 0) window.utils.requestAttention()
}
dispatch(saveServiceConfigs(configs))
return [parsedItems, configs]
} else {
return [[], configs]
}
},
syncItems: () => async (dispatch, getState) => {
const state = getState()
const configs = state.service as FeverConfigs
syncItems: () => async (_, getState) => {
const configs = getState().service as FeverConfigs
const unreadResponse = await fetchAPI(configs, "&unread_item_ids")
const starredResponse = await fetchAPI(configs, "&saved_item_ids")
if (typeof unreadResponse.unread_item_ids !== "string" || typeof starredResponse.saved_item_ids !== "string") {
@ -234,39 +161,7 @@ export const feverServiceHooks: ServiceHooks = {
}
const unreadFids: number[] = unreadResponse.unread_item_ids.split(",").map(s => parseInt(s))
const starredFids: number[] = starredResponse.saved_item_ids.split(",").map(s => parseInt(s))
const promises = new Array<Promise<number>>()
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $in: unreadFids },
hasRead: true
}, { $set: { hasRead: false } }, { multi: true }, (_, num) => resolve(num))
}))
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $nin: unreadFids },
hasRead: false
}, { $set: { hasRead: true } }, { multi: true }, (_, num) => resolve(num))
}))
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $in: starredFids },
starred: { $exists: false }
}, { $set: { starred: true } }, { multi: true }, (_, num) => resolve(num))
}))
promises.push(new Promise((resolve) => {
db.idb.update({
serviceRef: { $exists: true, $nin: starredFids },
starred: true
}, { $unset: { starred: true } }, { multi: true }, (_, num) => resolve(num))
}))
const affected = (await Promise.all(promises)).reduce((a, b) => a + b, 0)
if (affected > 0) {
dispatch(syncLocalItems(unreadFids, starredFids))
if (!(state.page.filter.type & FilterType.ShowRead) || !(state.page.filter.type & FilterType.ShowNotStarred)) {
dispatch(initFeeds(true))
}
await dispatch(updateUnreadCounts())
}
return [unreadFids, starredFids]
},
markAllRead: (sids, date, before) => async (_, getState) => {