db performance and schema changes

This commit is contained in:
刘浩远 2020-09-01 11:29:08 +08:00
parent 06757d0fcd
commit 110be62694
19 changed files with 198 additions and 155 deletions

View File

@ -106,8 +106,8 @@ const settingsBridge = {
getNeDBStatus: (): boolean => { getNeDBStatus: (): boolean => {
return ipcRenderer.sendSync("get-nedb-status") return ipcRenderer.sendSync("get-nedb-status")
}, },
setNeDBStatus: () => { setNeDBStatus: (flag: boolean) => {
ipcRenderer.invoke("set-nedb-status") ipcRenderer.invoke("set-nedb-status", flag)
}, },
getAll: () => { getAll: () => {

View File

@ -11,7 +11,7 @@ export type FeedProps = FeedReduxProps & {
viewType: ViewType viewType: ViewType
viewConfigs?: ViewConfigs viewConfigs?: ViewConfigs
items: RSSItem[] items: RSSItem[]
currentItem: string currentItem: number
sourceMap: Object sourceMap: Object
filter: FeedFilter filter: FeedFilter
shortcuts: (item: RSSItem, e: KeyboardEvent) => void shortcuts: (item: RSSItem, e: KeyboardEvent) => void

View File

@ -8,7 +8,7 @@ type LogMenuProps = {
display: boolean display: boolean
logs: AppLog[] logs: AppLog[]
close: () => void close: () => void
showItem: (iid: string) => void showItem: (iid: number) => void
} }
function getLogIcon(log: AppLog) { function getLogIcon(log: AppLog) {

View File

@ -10,7 +10,7 @@ type PageProps = {
contextOn: boolean contextOn: boolean
settingsOn: boolean settingsOn: boolean
feeds: string[] feeds: string[]
itemId: string itemId: number
itemFromFeed: boolean itemFromFeed: boolean
viewType: ViewType viewType: ViewType
dismissItem: () => void dismissItem: () => void

View File

@ -8,7 +8,7 @@ import Article from "../components/article"
import { openTextMenu, closeContextMenu, openImageMenu } from "../scripts/models/app" import { openTextMenu, closeContextMenu, openImageMenu } from "../scripts/models/app"
type ArticleContainerProps = { type ArticleContainerProps = {
itemId: string itemId: number
} }
const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId] const getItem = (state: RootState, props: ArticleContainerProps) => state.items[props.itemId]

View File

@ -12,7 +12,7 @@ const mapStateToProps = createSelector(getLogs, logs => logs)
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
close: () => dispatch(toggleLogMenu()), close: () => dispatch(toggleLogMenu()),
showItem: (iid: string) => dispatch(showItemFromId(iid)) showItem: (iid: number) => dispatch(showItemFromId(iid))
} }
} }

View File

@ -177,6 +177,6 @@ const NEDB_STATUS_STORE_KEY = "useNeDB"
ipcMain.on("get-nedb-status", (event) => { ipcMain.on("get-nedb-status", (event) => {
event.returnValue = store.get(NEDB_STATUS_STORE_KEY, true) event.returnValue = store.get(NEDB_STATUS_STORE_KEY, true)
}) })
ipcMain.handle("set-nedb-status", () => { ipcMain.handle("set-nedb-status", (_, flag: boolean) => {
store.set(NEDB_STATUS_STORE_KEY, false) store.set(NEDB_STATUS_STORE_KEY, flag)
}) })

View File

@ -7,7 +7,7 @@ export default function performUpdate(store: Store<SchemaTypes>) {
let useNeDB = store.get("useNeDB", undefined) let useNeDB = store.get("useNeDB", undefined)
let currentVersion = app.getVersion() let currentVersion = app.getVersion()
if (useNeDB === undefined) { if (useNeDB === undefined && version !== null) {
const revs = version.split(".").map(s => parseInt(s)) const revs = version.split(".").map(s => parseInt(s))
if ((revs[0] === 0 && revs[1] < 8) || !app.isPackaged) { if ((revs[0] === 0 && revs[1] < 8) || !app.isPackaged) {
store.set("useNeDB", true) store.set("useNeDB", true)

View File

@ -11,7 +11,7 @@ sdbSchema.createTable("sources").
addColumn("name", lf.Type.STRING). addColumn("name", lf.Type.STRING).
addColumn("openTarget", lf.Type.NUMBER). addColumn("openTarget", lf.Type.NUMBER).
addColumn("lastFetched", lf.Type.DATE_TIME). addColumn("lastFetched", lf.Type.DATE_TIME).
addColumn("serviceRef", lf.Type.NUMBER). addColumn("serviceRef", lf.Type.STRING).
addColumn("fetchFrequency", lf.Type.NUMBER). addColumn("fetchFrequency", lf.Type.NUMBER).
addColumn("rules", lf.Type.OBJECT). addColumn("rules", lf.Type.OBJECT).
addNullable(["iconurl", "serviceRef", "rules"]). addNullable(["iconurl", "serviceRef", "rules"]).
@ -33,9 +33,10 @@ idbSchema.createTable("items").
addColumn("starred", lf.Type.BOOLEAN). addColumn("starred", lf.Type.BOOLEAN).
addColumn("hidden", lf.Type.BOOLEAN). addColumn("hidden", lf.Type.BOOLEAN).
addColumn("notify", lf.Type.BOOLEAN). addColumn("notify", lf.Type.BOOLEAN).
addColumn("serviceRef", lf.Type.NUMBER). addColumn("serviceRef", lf.Type.STRING).
addNullable(["thumb", "creator", "serviceRef"]). addNullable(["thumb", "creator", "serviceRef"]).
addIndex("idxDate", ["date"], false, lf.Order.DESC) addIndex("idxDate", ["date"], false, lf.Order.DESC).
addIndex("idxService", ["serviceRef"], false)
export let sourcesDB: lf.Database export let sourcesDB: lf.Database
export let sources: lf.schema.Table export let sources: lf.schema.Table
@ -73,14 +74,14 @@ export async function init() {
}) })
}) })
const sRows = sourceDocs.map(doc => { const sRows = sourceDocs.map(doc => {
//doc.serviceRef = String(doc.serviceRef) if (doc.serviceRef !== undefined) doc.serviceRef = String(doc.serviceRef)
// @ts-ignore // @ts-ignore
delete doc._id delete doc._id
if (!doc.fetchFrequency) doc.fetchFrequency = 0 if (!doc.fetchFrequency) doc.fetchFrequency = 0
return sources.createRow(doc) return sources.createRow(doc)
}) })
const iRows = itemDocs.map(doc => { const iRows = itemDocs.map(doc => {
//doc.serviceRef = String(doc.serviceRef) if (doc.serviceRef !== undefined) doc.serviceRef = String(doc.serviceRef)
delete doc._id delete doc._id
doc.starred = Boolean(doc.starred) doc.starred = Boolean(doc.starred)
doc.hidden = Boolean(doc.hidden) doc.hidden = Boolean(doc.hidden)
@ -91,6 +92,8 @@ export async function init() {
sourcesDB.insert().into(sources).values(sRows).exec(), sourcesDB.insert().into(sources).values(sRows).exec(),
itemsDB.insert().into(items).values(iRows).exec() itemsDB.insert().into(items).values(iRows).exec()
]) ])
window.settings.setNeDBStatus() window.settings.setNeDBStatus(false)
sdb.remove({}, { multi: true }, () => { sdb.persistence.compactDatafile() })
idb.remove({}, { multi: true }, () => { idb.persistence.compactDatafile() })
} }
} }

View File

@ -22,10 +22,10 @@ export class AppLog {
type: AppLogType type: AppLogType
title: string title: string
details?: string details?: string
iid?: string iid?: number
time: Date time: Date
constructor(type: AppLogType, title: string, details: string=null, iid: string = null) { constructor(type: AppLogType, title: string, details: string=null, iid: number = null) {
this.type = type this.type = type
this.title = title this.title = title
this.details = details this.details = details
@ -121,7 +121,7 @@ interface ToggleLogMenuAction { type: typeof TOGGLE_LOGS }
interface PushNotificationAction { interface PushNotificationAction {
type: typeof PUSH_NOTIFICATION type: typeof PUSH_NOTIFICATION
iid: string iid: number
title: string title: string
source: string source: string
} }

View File

@ -84,7 +84,7 @@ export class RSSFeed {
loading: boolean loading: boolean
allLoaded: boolean allLoaded: boolean
sids: number[] sids: number[]
iids: string[] iids: number[]
filter: FeedFilter filter: FeedFilter
constructor (id: string = null, sids=[], filter=null) { constructor (id: string = null, sids=[], filter=null) {
@ -240,7 +240,7 @@ export function feedReducer(
switch (action.status) { switch (action.status) {
case ActionStatus.Success: return { case ActionStatus.Success: return {
...state, ...state,
[ALL]: new RSSFeed(ALL, action.sources.map(s => s.sid)) [ALL]: new RSSFeed(ALL, Object.values(action.sources).map(s => s.sid))
} }
default: return state default: return state
} }

View File

@ -9,7 +9,7 @@ import { pushNotification, setupAutoFetch } from "./app"
import { getServiceHooks, syncWithService, ServiceActionTypes, SYNC_LOCAL_ITEMS } from "./service" import { getServiceHooks, syncWithService, ServiceActionTypes, SYNC_LOCAL_ITEMS } from "./service"
export class RSSItem { export class RSSItem {
_id: string _id: number
source: number source: number
title: string title: string
link: string link: string
@ -23,7 +23,7 @@ export class RSSItem {
starred: boolean starred: boolean
hidden: boolean hidden: boolean
notify: boolean notify: boolean
serviceRef?: string | number serviceRef?: string
constructor (item: Parser.Item, source: RSSSource) { constructor (item: Parser.Item, source: RSSSource) {
for (let field of ["title", "link", "creator"]) { for (let field of ["title", "link", "creator"]) {
@ -79,7 +79,7 @@ export class RSSItem {
} }
export type ItemState = { export type ItemState = {
[_id: string]: RSSItem [_id: number]: RSSItem
} }
export const FETCH_ITEMS = "FETCH_ITEMS" export const FETCH_ITEMS = "FETCH_ITEMS"
@ -306,7 +306,7 @@ const toggleStarredDone = (item: RSSItem): ItemActionTypes => ({
export function toggleStarred(item: RSSItem): AppThunk { export function toggleStarred(item: RSSItem): AppThunk {
return (dispatch) => { return (dispatch) => {
if (item.starred === true) { if (item.starred) {
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.starred, false).exec() db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.starred, false).exec()
} else { } else {
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.starred, true).exec() db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.starred, true).exec()
@ -327,10 +327,10 @@ const toggleHiddenDone = (item: RSSItem): ItemActionTypes => ({
export function toggleHidden(item: RSSItem): AppThunk { export function toggleHidden(item: RSSItem): AppThunk {
return (dispatch) => { return (dispatch) => {
if (item.hidden === true) { if (item.hidden) {
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hidden, true).exec()
} else {
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hidden, false).exec() db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hidden, false).exec()
} else {
db.itemsDB.update(db.items).where(db.items._id.eq(item._id)).set(db.items.hidden, true).exec()
} }
dispatch(toggleHiddenDone(item)) dispatch(toggleHiddenDone(item))
} }
@ -371,7 +371,7 @@ export function applyItemReduction(item: RSSItem, type: string) {
break break
} }
case TOGGLE_HIDDEN: { case TOGGLE_HIDDEN: {
item.hidden = !item.hidden nextItem.hidden = !item.hidden
break break
} }
} }
@ -406,13 +406,13 @@ export function itemReducer(
case MARK_ALL_READ: { case MARK_ALL_READ: {
let nextState = { ...state } let nextState = { ...state }
let sids = new Set(action.sids) let sids = new Set(action.sids)
for (let [id, item] of Object.entries(state)) { for (let item of Object.values(state)) {
if (sids.has(item.source) && !item.hasRead) { if (sids.has(item.source) && !item.hasRead) {
if (!action.time || (action.before if (!action.time || (action.before
? item.date.getTime() <= action.time ? item.date.getTime() <= action.time
: item.date.getTime() >= action.time) : item.date.getTime() >= action.time)
) { ) {
nextState[id] = { nextState[item._id] = {
...item, ...item,
hasRead: true hasRead: true
} }
@ -435,15 +435,13 @@ export function itemReducer(
} }
} }
case SYNC_LOCAL_ITEMS: { case SYNC_LOCAL_ITEMS: {
const unreadSet = new Set(action.unreadIds)
const starredSet = new Set(action.starredIds)
let nextState = { ...state } let nextState = { ...state }
for (let [id, item] of Object.entries(state)) { for (let item of Object.values(state)) {
if (item.hasOwnProperty("serviceRef")) { if (item.hasOwnProperty("serviceRef")) {
const nextItem = { ...item } const nextItem = { ...item }
nextItem.hasRead = !unreadSet.has(nextItem.serviceRef as number) nextItem.hasRead = !action.unreadIds.has(item.serviceRef)
nextItem.starred = starredSet.has(item.serviceRef as number) nextItem.starred = action.starredIds.has(item.serviceRef)
nextState[id] = nextItem nextState[item._id] = nextItem
} }
} }
return nextState return nextState

View File

@ -114,7 +114,7 @@ export function showItem(feedId: string, item: RSSItem): AppThunk {
} }
} }
} }
export function showItemFromId(iid: string): AppThunk { export function showItemFromId(iid: number): AppThunk {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState() const state = getState()
const item = state.items[iid] const item = state.items[iid]
@ -237,7 +237,7 @@ export class PageState {
viewConfigs = window.settings.getViewConfigs(window.settings.getDefaultView()) viewConfigs = window.settings.getViewConfigs(window.settings.getDefaultView())
filter = new FeedFilter() filter = new FeedFilter()
feedId = ALL feedId = ALL
itemId = null as string itemId = null as number
itemFromFeed = true itemFromFeed = true
searchOn = false searchOn = false
} }

View File

@ -14,9 +14,9 @@ import { feedbinServiceHooks } from "./services/feedbin"
export interface ServiceHooks { export interface ServiceHooks {
authenticate?: (configs: ServiceConfigs) => Promise<boolean> authenticate?: (configs: ServiceConfigs) => Promise<boolean>
updateSources?: () => AppThunk<Promise<[RSSSource[], Map<number | string, string>]>> updateSources?: () => AppThunk<Promise<[RSSSource[], Map<string, string>]>>
fetchItems?: () => AppThunk<Promise<[RSSItem[], ServiceConfigs]>> fetchItems?: () => AppThunk<Promise<[RSSItem[], ServiceConfigs]>>
syncItems?: () => AppThunk<Promise<[(number | string)[], (number | string)[]]>> syncItems?: () => AppThunk<Promise<[Set<string>, Set<string>]>>
markRead?: (item: RSSItem) => AppThunk markRead?: (item: RSSItem) => AppThunk
markUnread?: (item: RSSItem) => AppThunk markUnread?: (item: RSSItem) => AppThunk
markAllRead?: (sids?: number[], date?: Date, before?: boolean) => AppThunk<Promise<void>> markAllRead?: (sids?: number[], date?: Date, before?: boolean) => AppThunk<Promise<void>>
@ -71,7 +71,7 @@ export function syncWithService(background = false): AppThunk<Promise<void>> {
function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<void>> { function updateSources(hook: ServiceHooks["updateSources"]): AppThunk<Promise<void>> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const [sources, groupsMap] = await dispatch(hook()) const [sources, groupsMap] = await dispatch(hook())
const existing = new Map<number | string, RSSSource>() const existing = new Map<string, RSSSource>()
for (let source of Object.values(getState().sources)) { for (let source of Object.values(getState().sources)) {
if (source.serviceRef) { if (source.serviceRef) {
existing.set(source.serviceRef, source) existing.set(source.serviceRef, source)
@ -138,29 +138,37 @@ function syncItems(hook: ServiceHooks["syncItems"]): AppThunk<Promise<void>> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = getState() const state = getState()
const [unreadRefs, starredRefs] = await dispatch(hook()) const [unreadRefs, starredRefs] = await dispatch(hook())
const promises = [ const unreadCopy = new Set(unreadRefs)
db.itemsDB.update(db.items).set(db.items.hasRead, false).where(lf.op.and( const starredCopy = new Set(starredRefs)
db.items.serviceRef.in(unreadRefs), const rows = await db.itemsDB.select(
db.items.hasRead.eq(true) db.items.serviceRef, db.items.hasRead, db.items.starred
)).exec(), ).from(db.items).where(lf.op.and(
db.itemsDB.update(db.items).set(db.items.hasRead, true).where(lf.op.and( db.items.serviceRef.isNotNull(),
lf.op.not(db.items.serviceRef.in(unreadRefs)), lf.op.or(db.items.hasRead.eq(false), db.items.starred.eq(true))
db.items.hasRead.eq(false) )).exec()
)).exec(), const updates = new Array<lf.query.Update>()
db.itemsDB.update(db.items).set(db.items.starred, true).where(lf.op.and( for (let row of rows) {
db.items.serviceRef.in(starredRefs), const serviceRef = row["serviceRef"]
db.items.starred.eq(false) if (row["hasRead"] === false && !unreadRefs.delete(serviceRef)) {
)).exec(), updates.push(db.itemsDB.update(db.items).set(db.items.hasRead, true).where(db.items.serviceRef.eq(serviceRef)))
db.itemsDB.update(db.items).set(db.items.starred, false).where(lf.op.and( }
lf.op.not(db.items.serviceRef.in(starredRefs)), if (row["starred"] === true && !starredRefs.delete(serviceRef)) {
db.items.hasRead.eq(true) updates.push(db.itemsDB.update(db.items).set(db.items.starred, false).where(db.items.serviceRef.eq(serviceRef)))
)).exec(), }
] }
await Promise.all(promises) for (let unread of unreadRefs) {
await dispatch(updateUnreadCounts()) updates.push(db.itemsDB.update(db.items).set(db.items.hasRead, false).where(db.items.serviceRef.eq(unread)))
dispatch(syncLocalItems(unreadRefs, starredRefs)) }
if (!(state.page.filter.type & FilterType.ShowRead) || !(state.page.filter.type & FilterType.ShowNotStarred)) { for (let starred of starredRefs) {
dispatch(initFeeds(true)) updates.push(db.itemsDB.update(db.items).set(db.items.starred, true).where(db.items.serviceRef.eq(starred)))
}
if (updates.length > 0) {
await db.itemsDB.createTransaction().exec(updates)
await dispatch(updateUnreadCounts())
dispatch(syncLocalItems(unreadCopy, starredCopy))
if (!(state.page.filter.type & FilterType.ShowRead) || !(state.page.filter.type & FilterType.ShowNotStarred)) {
dispatch(initFeeds(true))
}
} }
} }
} }
@ -224,8 +232,8 @@ interface SyncWithServiceAction {
interface SyncLocalItemsAction { interface SyncLocalItemsAction {
type: typeof SYNC_LOCAL_ITEMS type: typeof SYNC_LOCAL_ITEMS
unreadIds: (string | number)[] unreadIds: Set<string>
starredIds: (string | number)[] starredIds: Set<string>
} }
export type ServiceActionTypes = SaveServiceConfigsAction | SyncWithServiceAction | SyncLocalItemsAction export type ServiceActionTypes = SaveServiceConfigsAction | SyncWithServiceAction | SyncLocalItemsAction
@ -240,7 +248,7 @@ export function saveServiceConfigs(configs: ServiceConfigs): AppThunk {
} }
} }
function syncLocalItems(unread: (string | number)[], starred: (string | number)[]): ServiceActionTypes { function syncLocalItems(unread: Set<string>, starred: Set<string>): ServiceActionTypes {
return { return {
type: SYNC_LOCAL_ITEMS, type: SYNC_LOCAL_ITEMS,
unreadIds: unread, unreadIds: unread,

View File

@ -62,7 +62,7 @@ export const feedbinServiceHooks: ServiceHooks = {
const response = await fetchAPI(configs, "subscriptions.json") const response = await fetchAPI(configs, "subscriptions.json")
if (response.status !== 200) throw APIError() if (response.status !== 200) throw APIError()
const subscriptions: any[] = await response.json() const subscriptions: any[] = await response.json()
let groupsMap: Map<number, string> let groupsMap: Map<string, string>
if (configs.importGroups) { if (configs.importGroups) {
const tagsResponse = await fetchAPI(configs, "taggings.json") const tagsResponse = await fetchAPI(configs, "taggings.json")
if (tagsResponse.status !== 200) throw APIError() if (tagsResponse.status !== 200) throw APIError()
@ -75,12 +75,12 @@ export const feedbinServiceHooks: ServiceHooks = {
tagsSet.add(title) tagsSet.add(title)
dispatch(createSourceGroup(title)) dispatch(createSourceGroup(title))
} }
groupsMap.set(tag.feed_id, title) groupsMap.set(String(tag.feed_id), title)
} }
} }
const sources = subscriptions.map(s => { const sources = subscriptions.map(s => {
const source = new RSSSource(s.feed_url, s.title) const source = new RSSSource(s.feed_url, s.title)
source.serviceRef = s.feed_id source.serviceRef = String(s.feed_id)
return source return source
}) })
return [sources, groupsMap] return [sources, groupsMap]
@ -95,7 +95,7 @@ export const feedbinServiceHooks: ServiceHooks = {
if (unreadResponse.status !== 200 || starredResponse.status !== 200) throw APIError() if (unreadResponse.status !== 200 || starredResponse.status !== 200) throw APIError()
const unread = await unreadResponse.json() const unread = await unreadResponse.json()
const starred = await starredResponse.json() const starred = await starredResponse.json()
return [unread, starred] return [new Set(unread.map(i => String(i))), new Set(starred.map(i => String(i)))]
}, },
fetchItems: () => async (_, getState) => { fetchItems: () => async (_, getState) => {
@ -120,10 +120,10 @@ export const feedbinServiceHooks: ServiceHooks = {
) )
configs.lastId = items.reduce((m, n) => Math.max(m, n.id), configs.lastId) configs.lastId = items.reduce((m, n) => Math.max(m, n.id), configs.lastId)
if (items.length > 0) { if (items.length > 0) {
const fidMap = new Map<number, RSSSource>() const fidMap = new Map<string, RSSSource>()
for (let source of Object.values(state.sources)) { for (let source of Object.values(state.sources)) {
if (source.serviceRef) { if (source.serviceRef) {
fidMap.set(source.serviceRef as number, source) fidMap.set(source.serviceRef, source)
} }
} }
const [unreadResponse, starredResponse] = await Promise.all([ const [unreadResponse, starredResponse] = await Promise.all([
@ -134,7 +134,7 @@ export const feedbinServiceHooks: ServiceHooks = {
const unread: Set<number> = new Set(await unreadResponse.json()) const unread: Set<number> = new Set(await unreadResponse.json())
const starred: Set<number> = new Set(await starredResponse.json()) const starred: Set<number> = new Set(await starredResponse.json())
const parsedItems = items.map(i => { const parsedItems = items.map(i => {
const source = fidMap.get(i.feed_id) const source = fidMap.get(String(i.feed_id))
const dom = domParser.parseFromString(i.content, "text/html") const dom = domParser.parseFromString(i.content, "text/html")
const item = { const item = {
source: source.sid, source: source.sid,
@ -149,7 +149,7 @@ export const feedbinServiceHooks: ServiceHooks = {
starred: starred.has(i.id), starred: starred.has(i.id),
hidden: false, hidden: false,
notify: false, notify: false,
serviceRef: i.id, serviceRef: String(i.id),
} as RSSItem } as RSSItem
if (i.images && i.images.original_url) { if (i.images && i.images.original_url) {
item.thumb = i.images.original_url item.thumb = i.images.original_url
@ -186,23 +186,23 @@ export const feedbinServiceHooks: ServiceHooks = {
predicates.push(before ? db.items.date.lte(date) : db.items.date.gte(date)) predicates.push(before ? db.items.date.lte(date) : db.items.date.gte(date))
} }
const rows = await db.itemsDB.select(db.items.serviceRef).where(lf.op.and.apply(null, predicates)).exec() const rows = await db.itemsDB.select(db.items.serviceRef).where(lf.op.and.apply(null, predicates)).exec()
const refs = rows.map(row => row["serviceRef"]) as number[] const refs = rows.map(row => parseInt(row["serviceRef"]))
markItems(configs, "unread", "DELETE", refs) markItems(configs, "unread", "DELETE", refs)
}, },
markRead: (item: RSSItem) => async (_, getState) => { markRead: (item: RSSItem) => async (_, getState) => {
await markItems(getState().service as FeedbinConfigs, "unread", "DELETE", [item.serviceRef as number]) await markItems(getState().service as FeedbinConfigs, "unread", "DELETE", [parseInt(item.serviceRef)])
}, },
markUnread: (item: RSSItem) => async (_, getState) => { markUnread: (item: RSSItem) => async (_, getState) => {
await markItems(getState().service as FeedbinConfigs, "unread", "POST", [item.serviceRef as number]) await markItems(getState().service as FeedbinConfigs, "unread", "POST", [parseInt(item.serviceRef)])
}, },
star: (item: RSSItem) => async (_, getState) => { star: (item: RSSItem) => async (_, getState) => {
await markItems(getState().service as FeedbinConfigs, "starred", "POST", [item.serviceRef as number]) await markItems(getState().service as FeedbinConfigs, "starred", "POST", [parseInt(item.serviceRef)])
}, },
unstar: (item: RSSItem) => async (_, getState) => { unstar: (item: RSSItem) => async (_, getState) => {
await markItems(getState().service as FeedbinConfigs, "starred", "DELETE", [item.serviceRef as number]) await markItems(getState().service as FeedbinConfigs, "starred", "DELETE", [parseInt(item.serviceRef)])
}, },
} }

View File

@ -53,7 +53,7 @@ export const feverServiceHooks: ServiceHooks = {
const feeds: any[] = response.feeds const feeds: any[] = response.feeds
const feedGroups: any[] = response.feeds_groups const feedGroups: any[] = response.feeds_groups
if (feeds === undefined) throw APIError() if (feeds === undefined) throw APIError()
let groupsMap: Map<number, string> let groupsMap: Map<string, string>
if (configs.importGroups) { if (configs.importGroups) {
// Import groups on the first sync // Import groups on the first sync
const groups: any[] = (await fetchAPI(configs, "&groups")).groups const groups: any[] = (await fetchAPI(configs, "&groups")).groups
@ -66,14 +66,14 @@ export const feverServiceHooks: ServiceHooks = {
} }
groupsMap = new Map() groupsMap = new Map()
for (let group of feedGroups) { for (let group of feedGroups) {
for (let fid of group.feed_ids.split(",").map(s => parseInt(s))) { for (let fid of group.feed_ids.split(",")) {
groupsMap.set(fid, groupsIdMap.get(group.group_id)) groupsMap.set(fid, groupsIdMap.get(group.group_id))
} }
} }
} }
const sources = feeds.map(f => { const sources = feeds.map(f => {
const source = new RSSSource(f.url, f.title) const source = new RSSSource(f.url, f.title)
source.serviceRef = f.id source.serviceRef = String(f.id)
return source return source
}) })
return [sources, groupsMap] return [sources, groupsMap]
@ -104,14 +104,14 @@ export const feverServiceHooks: ServiceHooks = {
) )
configs.lastId = items.reduce((m, n) => Math.max(m, n.id), configs.lastId) configs.lastId = items.reduce((m, n) => Math.max(m, n.id), configs.lastId)
if (items.length > 0) { if (items.length > 0) {
const fidMap = new Map<number, RSSSource>() const fidMap = new Map<string, RSSSource>()
for (let source of Object.values(state.sources)) { for (let source of Object.values(state.sources)) {
if (source.serviceRef) { if (source.serviceRef) {
fidMap.set(source.serviceRef as number, source) fidMap.set(source.serviceRef, source)
} }
} }
const parsedItems = items.map(i => { const parsedItems = items.map(i => {
const source = fidMap.get(i.feed_id) const source = fidMap.get(String(i.feed_id))
const item = { const item = {
source: source.sid, source: source.sid,
title: i.title, title: i.title,
@ -125,7 +125,7 @@ export const feverServiceHooks: ServiceHooks = {
starred: Boolean(i.is_saved), starred: Boolean(i.is_saved),
hidden: false, hidden: false,
notify: false, notify: false,
serviceRef: typeof i.id === "string" ? parseInt(i.id) : i.id, serviceRef: String(i.id),
} as RSSItem } as RSSItem
// Try to get the thumbnail of the item // Try to get the thumbnail of the item
let dom = domParser.parseFromString(item.content, "text/html") let dom = domParser.parseFromString(item.content, "text/html")
@ -163,9 +163,9 @@ export const feverServiceHooks: ServiceHooks = {
if (typeof unreadResponse.unread_item_ids !== "string" || typeof starredResponse.saved_item_ids !== "string") { if (typeof unreadResponse.unread_item_ids !== "string" || typeof starredResponse.saved_item_ids !== "string") {
throw APIError() throw APIError()
} }
const unreadFids: number[] = unreadResponse.unread_item_ids.split(",").map(s => parseInt(s)) const unreadFids: string[] = unreadResponse.unread_item_ids.split(",")
const starredFids: number[] = starredResponse.saved_item_ids.split(",").map(s => parseInt(s)) const starredFids: string[] = starredResponse.saved_item_ids.split(",")
return [unreadFids, starredFids] return [new Set(unreadFids), new Set(starredFids)]
}, },
markAllRead: (sids, date, before) => async (_, getState) => { markAllRead: (sids, date, before) => async (_, getState) => {

View File

@ -19,7 +19,7 @@ export class RSSSource {
openTarget: SourceOpenTarget openTarget: SourceOpenTarget
unreadCount: number unreadCount: number
lastFetched: Date lastFetched: Date
serviceRef?: string | number serviceRef?: string
fetchFrequency: number // in minutes fetchFrequency: number // in minutes
rules?: SourceRule[] rules?: SourceRule[]
@ -81,12 +81,13 @@ export type SourceState = {
export const INIT_SOURCES = "INIT_SOURCES" export const INIT_SOURCES = "INIT_SOURCES"
export const ADD_SOURCE = "ADD_SOURCE" export const ADD_SOURCE = "ADD_SOURCE"
export const UPDATE_SOURCE = "UPDATE_SOURCE" export const UPDATE_SOURCE = "UPDATE_SOURCE"
export const UPDATE_UNREAD_COUNTS = "UPDATE_UNREAD_COUNTS"
export const DELETE_SOURCE = "DELETE_SOURCE" export const DELETE_SOURCE = "DELETE_SOURCE"
interface InitSourcesAction { interface InitSourcesAction {
type: typeof INIT_SOURCES type: typeof INIT_SOURCES
status: ActionStatus status: ActionStatus
sources?: RSSSource[] sources?: SourceState
err? err?
} }
@ -103,12 +104,18 @@ interface UpdateSourceAction {
source: RSSSource source: RSSSource
} }
interface UpdateUnreadCountsAction {
type: typeof UPDATE_UNREAD_COUNTS
sources: SourceState
}
interface DeleteSourceAction { interface DeleteSourceAction {
type: typeof DELETE_SOURCE, type: typeof DELETE_SOURCE,
source: RSSSource source: RSSSource
} }
export type SourceActionTypes = InitSourcesAction | AddSourceAction | UpdateSourceAction | DeleteSourceAction export type SourceActionTypes = InitSourcesAction | AddSourceAction | UpdateSourceAction
| UpdateUnreadCountsAction | DeleteSourceAction
export function initSourcesRequest(): SourceActionTypes { export function initSourcesRequest(): SourceActionTypes {
return { return {
@ -117,7 +124,7 @@ export function initSourcesRequest(): SourceActionTypes {
} }
} }
export function initSourcesSuccess(sources: RSSSource[]): SourceActionTypes { export function initSourcesSuccess(sources: SourceState): SourceActionTypes {
return { return {
type: INIT_SOURCES, type: INIT_SOURCES,
status: ActionStatus.Success, status: ActionStatus.Success,
@ -133,21 +140,34 @@ export function initSourcesFailure(err): SourceActionTypes {
} }
} }
async function unreadCount(source: RSSSource): Promise<RSSSource> { async function unreadCount(sources: SourceState): Promise<SourceState> {
source.unreadCount = (await db.itemsDB.select( const rows = await db.itemsDB.select(
db.items.source,
lf.fn.count(db.items._id) lf.fn.count(db.items._id)
).from(db.items).where(lf.op.and( ).from(db.items).where(
db.items.source.eq(source.sid),
db.items.hasRead.eq(false) db.items.hasRead.eq(false)
)).exec())[0]["COUNT(_id)"] ).groupBy(
return source db.items.source
).exec()
for (let row of rows) {
sources[row["source"]].unreadCount = row["COUNT(_id)"]
}
return sources
} }
export function updateUnreadCounts(): AppThunk<Promise<void>> { export function updateUnreadCounts(): AppThunk<Promise<void>> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
await Promise.all(Object.values(getState().sources).map(async s => { const sources: SourceState = {}
dispatch(updateSourceDone(await unreadCount(s))) for (let source of Object.values(getState().sources)) {
})) sources[source.sid] = {
...source,
unreadCount: 0
}
}
dispatch({
type: UPDATE_UNREAD_COUNTS,
sources: await unreadCount(sources),
})
} }
} }
@ -156,9 +176,13 @@ export function initSources(): AppThunk<Promise<void>> {
dispatch(initSourcesRequest()) dispatch(initSourcesRequest())
await db.init() await db.init()
const sources = (await db.sourcesDB.select().from(db.sources).exec()) as RSSSource[] const sources = (await db.sourcesDB.select().from(db.sources).exec()) as RSSSource[]
const promises = sources.map(s => unreadCount(s)) const state: SourceState = {}
const counted = await Promise.all(promises) for (let source of sources) {
dispatch(initSourcesSuccess(counted)) source.unreadCount = 0
state[source.sid] = source
}
await unreadCount(sources)
dispatch(initSourcesSuccess(sources))
} }
} }
@ -313,15 +337,10 @@ export function sourceReducer(
switch (action.type) { switch (action.type) {
case INIT_SOURCES: case INIT_SOURCES:
switch (action.status) { switch (action.status) {
case ActionStatus.Success: { case ActionStatus.Success: return action.sources
let newState: SourceState = {}
for (let source of action.sources) {
newState[source.sid] = source
}
return newState
}
default: return state default: return state
} }
case UPDATE_UNREAD_COUNTS: return action.sources
case ADD_SOURCE: case ADD_SOURCE:
switch (action.status) { switch (action.status) {
case ActionStatus.Success: return { case ActionStatus.Success: return {

View File

@ -1,3 +1,4 @@
import * as db from "./db"
import { IPartialTheme, loadTheme } from "@fluentui/react" import { IPartialTheme, loadTheme } from "@fluentui/react"
import locales from "./i18n/_locales" import locales from "./i18n/_locales"
import { ThemeSettings } from "../schema-types" import { ThemeSettings } from "../schema-types"
@ -57,29 +58,17 @@ export function getCurrentLocale() {
return (locale in locales) ? locale : "en-US" return (locale in locales) ? locale : "en-US"
} }
export function exportAll() { export async function exportAll() {
const filters = [{ name: intl.get("app.frData"), extensions: ["frdata"] }] const filters = [{ name: intl.get("app.frData"), extensions: ["frdata"] }]
window.utils.showSaveDialog(filters, "*/Fluent_Reader_Backup.frdata").then(write => { const write = await window.utils.showSaveDialog(filters, "*/Fluent_Reader_Backup.frdata")
if (write) { if (write) {
let output = window.settings.getAll() let output = window.settings.getAll()
output["nedb"] = {} output["lovefield"] = {
let openRequest = window.indexedDB.open("NeDB") sources: await db.sourcesDB.select().from(db.sources).exec(),
openRequest.onsuccess = () => { items: await db.itemsDB.select().from(db.items).exec(),
let db = openRequest.result
let objectStore = db.transaction("nedbdata").objectStore("nedbdata")
let cursorRequest = objectStore.openCursor()
cursorRequest.onsuccess = () => {
let cursor = cursorRequest.result
if (cursor) {
output["nedb"][cursor.key] = cursor.value
cursor.continue()
} else {
write(JSON.stringify(output), intl.get("settings.writeError"))
}
}
}
} }
}) write(JSON.stringify(output), intl.get("settings.writeError"))
}
} }
export async function importAll() { export async function importAll() {
@ -94,21 +83,40 @@ export async function importAll() {
) )
if (!confirmed) return true if (!confirmed) return true
let configs = JSON.parse(data) let configs = JSON.parse(data)
let openRequest = window.indexedDB.open("NeDB") await db.sourcesDB.delete().from(db.sources).exec()
openRequest.onsuccess = () => { await db.itemsDB.delete().from(db.items).exec()
let db = openRequest.result if (configs.nedb) {
let objectStore = db.transaction("nedbdata", "readwrite").objectStore("nedbdata") let openRequest = window.indexedDB.open("NeDB")
let requests = Object.entries(configs.nedb).map(([key, value]) => { configs.useNeDB = true
return objectStore.put(value, key) openRequest.onsuccess = () => {
let db = openRequest.result
let objectStore = db.transaction("nedbdata", "readwrite").objectStore("nedbdata")
let requests = Object.entries(configs.nedb).map(([key, value]) => {
return objectStore.put(value, key)
})
let promises = requests.map(req => new Promise((resolve, reject) => {
req.onsuccess = () => resolve()
req.onerror = () => reject()
}))
Promise.all(promises).then(() => {
delete configs.nedb
window.settings.setAll(configs)
})
}
} else {
const sRows = configs.lovefield.sources.map(s => {
s.lastFetched = new Date(s.lastFetched)
return db.sources.createRow(s)
}) })
let promises = requests.map(req => new Promise((resolve, reject) => { const iRows = configs.lovefield.items.map(i => {
req.onsuccess = () => resolve() i.date = new Date(i.date)
req.onerror = () => reject() i.fetchedDate = new Date(i.fetchedDate)
})) return db.items.createRow(i)
Promise.all(promises).then(() => {
delete configs.nedb
window.settings.setAll(configs)
}) })
} await db.sourcesDB.insert().into(db.sources).values(sRows).exec()
await db.itemsDB.insert().into(db.items).values(iRows).exec()
delete configs.lovefield
window.settings.setAll(configs)
}
return false return false
} }

View File

@ -189,15 +189,22 @@ function byteLength(str: string) {
export function calculateItemSize(): Promise<number> { export function calculateItemSize(): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let openRequest = window.indexedDB.open("NeDB") let result = 0
let openRequest = window.indexedDB.open("itemsDB")
openRequest.onsuccess = () => { openRequest.onsuccess = () => {
let db = openRequest.result let db = openRequest.result
let objectStore = db.transaction("nedbdata").objectStore("nedbdata") let objectStore = db.transaction("items").objectStore("items")
let getRequest = objectStore.get("items") let cursorRequest = objectStore.openCursor()
getRequest.onsuccess = () => { cursorRequest.onsuccess = () => {
resolve(byteLength(getRequest.result)) let cursor = cursorRequest.result
if (cursor) {
result += byteLength(JSON.stringify(cursor.value))
cursor.continue()
} else {
resolve(result)
}
} }
getRequest.onerror = () => reject() cursorRequest.onerror = () => reject()
} }
openRequest.onerror = () => reject() openRequest.onerror = () => reject()
}) })