refactor(desktop): remove old database and service (#3911)
This commit is contained in:
parent
be45c9b8f5
commit
52dd557d3d
|
|
@ -48,7 +48,6 @@
|
|||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"dayjs": "1.11.13",
|
||||
"dexie": "4.0.11",
|
||||
"dnum": "2.15.0",
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"embla-carousel-wheel-gestures": "8.0.2",
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const LOCAL_DB_NAME = "FOLLOW_DB"
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { afterEach } from "node:test"
|
||||
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { browserDB } from "./db"
|
||||
|
||||
describe("upgradeToV8", () => {
|
||||
afterEach(async () => {
|
||||
await browserDB.delete()
|
||||
})
|
||||
|
||||
it("should set tipUsers to an empty array if tipUsers is not an array", async () => {
|
||||
const insertFeeds = [
|
||||
{ id: 1, tipUsers: {} },
|
||||
{ id: 2, tipUsers: null },
|
||||
{ id: 3, tipUsers: [{ name: "user1" }] },
|
||||
]
|
||||
// @ts-expect-error
|
||||
await browserDB.feeds.bulkAdd(insertFeeds)
|
||||
|
||||
const feeds = await browserDB.feeds.toArray()
|
||||
expect(feeds.length).toEqual(3)
|
||||
expect(feeds[0]!.tipUsers).toEqual(insertFeeds[0]!.tipUsers)
|
||||
expect(feeds[1]!.tipUsers).toEqual(insertFeeds[1]!.tipUsers)
|
||||
expect(feeds[2]!.tipUsers).toEqual(insertFeeds[2]!.tipUsers)
|
||||
|
||||
await browserDB.transaction("rw", [browserDB.feeds], async (tx) => {
|
||||
await browserDB.upgradeToV8(tx)
|
||||
})
|
||||
const feedsAfterMigrate = await browserDB.feeds.toArray()
|
||||
expect(feedsAfterMigrate.length).toEqual(3)
|
||||
expect(feedsAfterMigrate[0]!.tipUsers).toEqual([])
|
||||
expect(feedsAfterMigrate[1]!.tipUsers).toEqual(null)
|
||||
expect(feedsAfterMigrate[2]!.tipUsers).toEqual([{ name: "user1" }])
|
||||
})
|
||||
})
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import type { Transaction } from "dexie"
|
||||
import Dexie from "dexie"
|
||||
|
||||
import { LOCAL_DB_NAME } from "./constants"
|
||||
import {
|
||||
dbSchemaV1,
|
||||
dbSchemaV2,
|
||||
dbSchemaV3,
|
||||
dbSchemaV4,
|
||||
dbSchemaV5,
|
||||
dbSchemaV6,
|
||||
dbSchemaV7,
|
||||
dbSchemaV8,
|
||||
} from "./db_schema"
|
||||
import type { DB_Cleaner } from "./schemas/cleaner"
|
||||
import type { DB_Entry, DB_EntryRelated } from "./schemas/entry"
|
||||
import type { DB_Feed, DB_FeedUnread } from "./schemas/feed"
|
||||
import type { DB_Inbox } from "./schemas/inbox"
|
||||
import type { DB_List } from "./schemas/list"
|
||||
import type { DB_Subscription } from "./schemas/subscription"
|
||||
|
||||
export interface LocalDBSchemaMap {
|
||||
entries: DB_Entry
|
||||
feeds: DB_Feed
|
||||
subscriptions: DB_Subscription
|
||||
entryRelated: DB_EntryRelated
|
||||
feedUnreads: DB_FeedUnread
|
||||
cleaner: DB_Cleaner
|
||||
lists: DB_List
|
||||
inboxes: DB_Inbox
|
||||
}
|
||||
|
||||
// Define a local DB
|
||||
class BrowserDB extends Dexie {
|
||||
public entries: BrowserDBTable<"entries">
|
||||
public feeds: BrowserDBTable<"feeds">
|
||||
public subscriptions: BrowserDBTable<"subscriptions">
|
||||
public entryRelated: BrowserDBTable<"entryRelated">
|
||||
public feedUnreads: BrowserDBTable<"feedUnreads">
|
||||
public lists: BrowserDBTable<"lists">
|
||||
public inboxes: BrowserDBTable<"inboxes">
|
||||
public cleaner: BrowserDBTable<"cleaner">
|
||||
|
||||
constructor() {
|
||||
super(LOCAL_DB_NAME)
|
||||
this.version(1).stores(dbSchemaV1)
|
||||
this.version(2).stores(dbSchemaV2).upgrade(this.upgradeToV2)
|
||||
this.version(3).stores(dbSchemaV3)
|
||||
this.version(4).stores(dbSchemaV4)
|
||||
this.version(5).stores(dbSchemaV5)
|
||||
this.version(6).stores(dbSchemaV6)
|
||||
this.version(7).stores(dbSchemaV7)
|
||||
this.version(8).stores(dbSchemaV8).upgrade(this.upgradeToV8)
|
||||
|
||||
this.entries = this.table("entries")
|
||||
this.feeds = this.table("feeds")
|
||||
this.subscriptions = this.table("subscriptions")
|
||||
this.entryRelated = this.table("entryRelated")
|
||||
this.feedUnreads = this.table("feedUnreads")
|
||||
this.cleaner = this.table("cleaner")
|
||||
this.lists = this.table("lists")
|
||||
this.inboxes = this.table("inboxes")
|
||||
}
|
||||
|
||||
async upgradeToV2(trans: Transaction) {
|
||||
const session = trans.table("feedUnreads")
|
||||
session.delete("feedId")
|
||||
}
|
||||
|
||||
async upgradeToV8(trans: Transaction) {
|
||||
// Fix https://github.com/RSSNext/Follow/issues/1308
|
||||
const session = trans.table("feeds")
|
||||
return session.toCollection().modify((feed) => {
|
||||
if (!feed.tipUsers || Array.isArray(feed.tipUsers)) return
|
||||
feed.tipUsers = []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const browserDB = new BrowserDB()
|
||||
|
||||
// ================================================ //
|
||||
// ================================================ //
|
||||
// ================================================ //
|
||||
// ================================================ //
|
||||
// ================================================ //
|
||||
|
||||
// types helper
|
||||
export type BrowserDBSchema = {
|
||||
[t in keyof LocalDBSchemaMap]: {
|
||||
model: LocalDBSchemaMap[t]
|
||||
table: Dexie.Table<LocalDBSchemaMap[t], string>
|
||||
}
|
||||
}
|
||||
type BrowserDBTable<T extends keyof LocalDBSchemaMap> = BrowserDBSchema[T]["table"]
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
export const dbSchemaV1 = {
|
||||
entries: "&id",
|
||||
feeds: "&id",
|
||||
subscriptions: "&id",
|
||||
entryRelated: "&id",
|
||||
feedUnreads: "&id",
|
||||
}
|
||||
|
||||
export const dbSchemaV2 = {
|
||||
...dbSchemaV1,
|
||||
subscriptions: "&id, userId, feedId",
|
||||
}
|
||||
|
||||
export const dbSchemaV3 = {
|
||||
...dbSchemaV2,
|
||||
feedEntries: null,
|
||||
|
||||
subscriptions: "&id, userId, &feedId",
|
||||
}
|
||||
|
||||
export const dbSchemaV4 = {
|
||||
...dbSchemaV3,
|
||||
entries: "&id, feedId",
|
||||
subscriptions: "&id, userId, feedId",
|
||||
}
|
||||
|
||||
export const dbSchemaV5 = {
|
||||
...dbSchemaV4,
|
||||
cleaner: "&refId, visitedAt",
|
||||
}
|
||||
|
||||
export const dbSchemaV6 = {
|
||||
...dbSchemaV5,
|
||||
lists: "&id, title",
|
||||
}
|
||||
|
||||
export const dbSchemaV7 = {
|
||||
...dbSchemaV6,
|
||||
inboxes: "&id",
|
||||
}
|
||||
|
||||
export const dbSchemaV8 = dbSchemaV7
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
declare global {
|
||||
// This flag controls write data in indexedDB, if it's false, pass data insert to db
|
||||
// When app not ready, it's false, after hydrate data, it's true
|
||||
// Or set is false when disable indexedDB in setting
|
||||
|
||||
export let __dbIsReady: boolean
|
||||
|
||||
interface Window {
|
||||
__dbIsReady: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { browserDB } from "./db"
|
||||
|
||||
export * from "./db"
|
||||
export * from "./schemas"
|
||||
|
||||
export const DB_NOT_READY_OR_DISABLED = "Database is not ready or disabled"
|
||||
/**
|
||||
* @description Check if database is ready
|
||||
* If users disabled data persist, it's always false, that means you can't do operation with database.
|
||||
*
|
||||
*/
|
||||
|
||||
export const runTransactionInScope = <T>(
|
||||
fn: (db: typeof browserDB) => T,
|
||||
): T | typeof DB_NOT_READY_OR_DISABLED => {
|
||||
if (!window.__dbIsReady) {
|
||||
// Or, push to waiting queue
|
||||
return DB_NOT_READY_OR_DISABLED
|
||||
}
|
||||
|
||||
return fn(browserDB)
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { z } from "zod"
|
||||
|
||||
export const DB_BaseSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
export type DB_Base = z.infer<typeof DB_BaseSchema>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export type CleanerType = "feed" | "entry" | "list" | "inbox"
|
||||
export type DB_Cleaner = {
|
||||
refId: string
|
||||
visitedAt: number
|
||||
type: CleanerType
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { EntryModel } from "@follow/models/types"
|
||||
|
||||
export type DB_Entry = EntryModel & { feedId: string }
|
||||
|
||||
export type DB_EntryRelated = {
|
||||
id: string
|
||||
data: any
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import type { FeedModel } from "@follow/models/types"
|
||||
|
||||
export type DB_FeedUnread = {
|
||||
id: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type DB_Feed = FeedModel & { id: string }
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export type DB_Inbox = {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./base"
|
||||
export * from "./feed"
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
export type DB_List = {
|
||||
id: string
|
||||
title: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
description: string
|
||||
fee: number
|
||||
image: string
|
||||
ownerUserId: string
|
||||
timelineUpdatedAt: string
|
||||
feedIds: string[]
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export type DB_Subscription = any & {
|
||||
id: string
|
||||
}
|
||||
|
|
@ -1,44 +1,6 @@
|
|||
import { sleep } from "@follow/utils/utils"
|
||||
|
||||
import { initializeDefaultGeneralSettings } from "~/atoms/settings/general"
|
||||
import { initializeDefaultIntegrationSettings } from "~/atoms/settings/integration"
|
||||
import { initializeDefaultUISettings } from "~/atoms/settings/ui"
|
||||
import { appLog } from "~/lib/log"
|
||||
import { EntryService, FeedService, SubscriptionService, UnreadService } from "~/services"
|
||||
import { InboxService } from "~/services/inbox"
|
||||
import type { Hydratable } from "~/services/interface"
|
||||
import { ListService } from "~/services/list"
|
||||
|
||||
export const setHydrated = (v: boolean) => {
|
||||
window.__dbIsReady = v
|
||||
}
|
||||
|
||||
export const hydrateDatabaseToStore = async () => {
|
||||
async function hydrate() {
|
||||
const now = Date.now()
|
||||
|
||||
const hydrates: Hydratable[] = [
|
||||
FeedService,
|
||||
SubscriptionService,
|
||||
UnreadService,
|
||||
EntryService,
|
||||
ListService,
|
||||
InboxService,
|
||||
]
|
||||
await Promise.all(hydrates.map((h) => h.hydrate()))
|
||||
|
||||
window.__dbIsReady = true
|
||||
const costTime = Date.now() - now
|
||||
|
||||
return costTime
|
||||
}
|
||||
return Promise.race([hydrate(), sleep(1000).then(() => 10e10)]).then((result) => {
|
||||
if (result === 10e10) {
|
||||
appLog("Hydrate data timeout")
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export const hydrateSettings = () => {
|
||||
initializeDefaultUISettings()
|
||||
|
|
|
|||
|
|
@ -6,32 +6,19 @@ import { tracker } from "@follow/tracker"
|
|||
import { repository } from "@pkg"
|
||||
import { enableMapSet } from "immer"
|
||||
|
||||
import { browserDB } from "~/database"
|
||||
import { initI18n } from "~/i18n"
|
||||
import { settingSyncQueue } from "~/modules/settings/helper/sync-queue"
|
||||
import { ElectronCloseEvent, ElectronShowEvent } from "~/providers/invalidate-query-provider"
|
||||
import { CleanerService } from "~/services/cleaner"
|
||||
|
||||
import { subscribeNetworkStatus } from "../atoms/network"
|
||||
import { getGeneralSettings, subscribeShouldUseIndexedDB } from "../atoms/settings/general"
|
||||
import { getGeneralSettings } from "../atoms/settings/general"
|
||||
import { appLog } from "../lib/log"
|
||||
import { initAnalytics } from "./analytics"
|
||||
import { registerHistoryStack } from "./history"
|
||||
import { hydrateSettings, setHydrated } from "./hydrate"
|
||||
import { hydrateSettings } from "./hydrate"
|
||||
import { doMigration } from "./migrates"
|
||||
import { initSentry } from "./sentry"
|
||||
|
||||
const cleanup = subscribeShouldUseIndexedDB((value) => {
|
||||
if (!value) {
|
||||
browserDB.tables.forEach((table) => {
|
||||
table.clear()
|
||||
})
|
||||
setHydrated(false)
|
||||
return
|
||||
}
|
||||
setHydrated(true)
|
||||
})
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
version: string
|
||||
|
|
@ -109,8 +96,6 @@ export const initializeApp = async () => {
|
|||
migrateDatabase: true,
|
||||
})
|
||||
})
|
||||
|
||||
CleanerService.cleanOutdatedData()
|
||||
}
|
||||
|
||||
await apm("initAnalytics", initAnalytics)
|
||||
|
|
@ -128,8 +113,6 @@ export const initializeApp = async () => {
|
|||
})
|
||||
}
|
||||
|
||||
import.meta.hot?.dispose(cleanup)
|
||||
|
||||
const apm = async (label: string, fn: () => Promise<any> | any) => {
|
||||
const start = Date.now()
|
||||
const result = await fn()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useEffect } from "react"
|
|||
import { setUserRole, setWhoami } from "~/atoms/user"
|
||||
import { setIntegrationIdentify } from "~/initialize/helper"
|
||||
import { useSession } from "~/queries/auth"
|
||||
import { CleanerService } from "~/services/cleaner"
|
||||
|
||||
export const UserProvider = () => {
|
||||
const { session } = useSession()
|
||||
|
|
@ -15,9 +14,8 @@ export const UserProvider = () => {
|
|||
if (session.role) {
|
||||
setUserRole(session.role as UserRole)
|
||||
}
|
||||
// @ts-expect-error FIXME
|
||||
setIntegrationIdentify(session.user)
|
||||
|
||||
CleanerService.cleanRemainingData(session.user.id)
|
||||
}, [session?.role, session?.user])
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -1,228 +0,0 @@
|
|||
[
|
||||
{
|
||||
"read": true,
|
||||
"view": 0,
|
||||
"entries": {
|
||||
"id": "46877798792098816",
|
||||
"title": "DWMBlurGlass – 为 Windows 10/11 启用窗口透明/玻璃/模糊效果",
|
||||
"url": "https://www.appinn.com/dwmblurglass/",
|
||||
"description": "DWMBlurGlass 是一款为 Win10、Win11 全局系统标题栏添加自定义效果的开源工具,主要有模糊、混合颜色、文本颜色、Aero 效果等。@Appinn 来自发现频道,@suntrise 同学的推荐:https://meta.appinn.net/t/topic/59864\n\nDWMBlurGlass:为 Windows 10/11 启用窗口透明/玻璃/模糊效果\n\n今年,又有一款能让窗口标题栏拥有透明效果的应用 DWMBlurGlass 出现,不仅支持 Win10 2004+ 和 Win11,而且还是免费开源的。…",
|
||||
"guid": "https://www.appinn.com/?p=48489",
|
||||
"author": "青小蛙",
|
||||
"authorUrl": null,
|
||||
"authorAvatar": null,
|
||||
"insertedAt": "2024-08-15T11:59:12.267Z",
|
||||
"publishedAt": "2024-08-15T11:34:12.839Z",
|
||||
"media": [
|
||||
{
|
||||
"url": "https://www.appinn.com/wp-content/uploads/2024/08/Appinn-feature-images-2024-08-15T185350.869.jpg",
|
||||
"type": "photo",
|
||||
"width": 1608,
|
||||
"height": 700
|
||||
},
|
||||
{
|
||||
"url": "https://www.appinn.com/wp-content/uploads/2024/08/001701-1.avif",
|
||||
"type": "photo"
|
||||
},
|
||||
{
|
||||
"url": "https://www.appinn.com/wp-content/uploads/2024/08/013521.avif",
|
||||
"type": "photo"
|
||||
},
|
||||
{
|
||||
"url": "https://www.appinn.com/wp-content/uploads/2024/08/e3c13f90a0c5d7ca478d7.avif",
|
||||
"type": "photo"
|
||||
}
|
||||
],
|
||||
"categories": ["Windows", "美化主题", "模糊", "玻璃", "透明"],
|
||||
"attachments": null
|
||||
},
|
||||
"feeds": {
|
||||
"id": "41370691926515712",
|
||||
"url": "https://www.appinn.com/feed/",
|
||||
"title": "小众软件",
|
||||
"description": "分享免费、小巧、实用、有趣、绿色的软件",
|
||||
"siteUrl": "https://www.appinn.com/",
|
||||
"image": null,
|
||||
"checkedAt": "2024-08-15T11:59:12.267Z",
|
||||
"lastModifiedHeader": "Thu, 15 Aug 2024 11:34:18 GMT",
|
||||
"etagHeader": "W/\"74dc42ccaecdc9834948c29b13261d87\"",
|
||||
"ttl": 30,
|
||||
"errorMessage": null,
|
||||
"errorAt": null,
|
||||
"ownerUserId": null
|
||||
},
|
||||
"collections": null,
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"read": true,
|
||||
"view": 0,
|
||||
"entries": {
|
||||
"id": "46871301655897088",
|
||||
"title": "开放麒麟 openKylin 新增 LTS 长期支持版,采用“创新版本 + LTS 版本”双轨并行策略",
|
||||
"url": "https://www.ithome.com/0/788/779.htm",
|
||||
"description": "IT 之家 8 月 15 日消息,开放原子开源基金会上周刚刚发布了最新的 openKylin 2.0 版本!该版本汇聚了超过 6500 + 开发者的智慧与汗水,并得到了 110+ SIG 和 520 + 企业的鼎力支持。经过社区投票决议,openKylin 今天宣布将推出长期支持版本(LTS),未来,社区将采用“创新版本 + LTS 版本”双轨并行策略:\n\n创新版本:每年发布 1 个版本,提供 1 年被动更新支持\n\nLTS 版本:每 3 年发布 1 个版本,提供 1 年主动更新支持 + 1 年被动更新支持\n\nopenKylin 介绍称,LTS…",
|
||||
"guid": "https://www.ithome.com/0/788/779.htm",
|
||||
"author": null,
|
||||
"authorUrl": null,
|
||||
"authorAvatar": null,
|
||||
"insertedAt": "2024-08-15T11:33:04.823Z",
|
||||
"publishedAt": "2024-08-15T11:11:47.914Z",
|
||||
"media": [
|
||||
{
|
||||
"url": "https://img.ithome.com/newsuploadfiles/2024/8/1145ab09-45b1-4af4-a6e4-7bbec2548cfe.png?x-bce-process=image/format,f_auto",
|
||||
"type": "photo",
|
||||
"width": 1080,
|
||||
"height": 448
|
||||
}
|
||||
],
|
||||
"categories": null,
|
||||
"attachments": null
|
||||
},
|
||||
"feeds": {
|
||||
"id": "41397633064960000",
|
||||
"url": "https://www.ithome.com/rss/",
|
||||
"title": "IT 之家",
|
||||
"description": "IT 之家 - 软媒旗下网站",
|
||||
"siteUrl": "https://www.ithome.com/",
|
||||
"image": null,
|
||||
"checkedAt": "2024-08-15T11:33:04.823Z",
|
||||
"lastModifiedHeader": null,
|
||||
"etagHeader": null,
|
||||
"ttl": 30,
|
||||
"errorMessage": null,
|
||||
"errorAt": null,
|
||||
"ownerUserId": null
|
||||
},
|
||||
"collections": null,
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"read": true,
|
||||
"view": 0,
|
||||
"entries": {
|
||||
"id": "46871301655897089",
|
||||
"title": "微星海外推出 2024 款 Pro DP21 系列迷你主机:升级 14 代酷睿处理器、自带“古老”COM 串口",
|
||||
"url": "https://www.ithome.com/0/788/778.htm",
|
||||
"description": "IT 之家 8 月 15 日消息,据微星官网,微星今天在海外推出 2024 款 Pro DP21 系列迷你主机,该迷你主机主要升级 14 代酷睿处理器,适用于商务应用场景,截至 IT 之家发稿,官网还未显示具体售价信息。外观方面,这款迷你主机尺寸为 204 x 208 x 54.8mm,重 1.52 千克,整体有点类似 CD 光驱,正面采用格栅式设计,侧面和顶部配备进气孔,支持通过 VESA 支架壁挂。\n\n规格方面,该机基于英特尔 Q670 主板,可选 20 核心 28 线程酷睿 i7-14700 或 14 核心 20 线程酷睿 i5-14500 处理器,至高可选…",
|
||||
"guid": "https://www.ithome.com/0/788/778.htm",
|
||||
"author": null,
|
||||
"authorUrl": null,
|
||||
"authorAvatar": null,
|
||||
"insertedAt": "2024-08-15T11:33:04.823Z",
|
||||
"publishedAt": "2024-08-15T11:06:25.894Z",
|
||||
"media": [
|
||||
{
|
||||
"url": "https://img.ithome.com/newsuploadfiles/2024/8/dc1110c9-483a-4b6d-84cc-b229ec92a7b4.png",
|
||||
"type": "photo",
|
||||
"width": 1440,
|
||||
"height": 668
|
||||
},
|
||||
{
|
||||
"url": "https://img.ithome.com/newsuploadfiles/2024/8/df476b81-144a-4fbd-b0bb-6d81c9734b76.png",
|
||||
"type": "photo",
|
||||
"width": 1210,
|
||||
"height": 862
|
||||
},
|
||||
{
|
||||
"url": "https://img.ithome.com/newsuploadfiles/2024/8/568aaf73-5501-4562-9f11-ab77d654c733.png",
|
||||
"type": "photo",
|
||||
"width": 1440,
|
||||
"height": 611
|
||||
},
|
||||
{
|
||||
"url": "https://img.ithome.com/newsuploadfiles/2024/8/6120d871-2337-42d3-acb9-f8694362f63a.png",
|
||||
"type": "photo",
|
||||
"width": 1440,
|
||||
"height": 575
|
||||
},
|
||||
{
|
||||
"url": "https://img.ithome.com/newsuploadfiles/2024/8/e0dffc94-9746-4fc8-bbde-175a13af4115.png",
|
||||
"type": "photo",
|
||||
"width": 1440,
|
||||
"height": 866
|
||||
}
|
||||
],
|
||||
"categories": null,
|
||||
"attachments": null
|
||||
},
|
||||
"feeds": {
|
||||
"id": "41397633064960000",
|
||||
"url": "https://www.ithome.com/rss/",
|
||||
"title": "IT 之家",
|
||||
"description": "IT 之家 - 软媒旗下网站",
|
||||
"siteUrl": "https://www.ithome.com/",
|
||||
"image": null,
|
||||
"checkedAt": "2024-08-15T11:33:04.823Z",
|
||||
"lastModifiedHeader": null,
|
||||
"etagHeader": null,
|
||||
"ttl": 30,
|
||||
"errorMessage": null,
|
||||
"errorAt": null,
|
||||
"ownerUserId": null
|
||||
},
|
||||
"collections": null,
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"read": true,
|
||||
"view": 0,
|
||||
"entries": {
|
||||
"id": "46871301655897090",
|
||||
"title": "半年低至 88 元:百度网盘 SVIP 官方预售 0 点开始",
|
||||
"url": "https://www.ithome.com/0/788/777.htm",
|
||||
"description": "百度网盘超级会员年卡官方售价 298 元,今日半年卡预告将在 8 月 16 日 0 点开启 88 元预售狂促。约合 14.66 元 / 月、176 元 / 年好价:\n\n天猫百度网盘 超级会员 半年卡填登录手机号 16 日 0 点 88 元直达链接\n\n注:现购买权益时长 + 已有权益时长,不能超过 5 年,否则会充值失败。\n\n百度网盘 SVIP 特权:\n\n天猫百度网盘 超级会员 半年卡填登录手机号 16 日 0 点 88 元直达链接",
|
||||
"guid": "https://www.ithome.com/0/788/777.htm",
|
||||
"author": null,
|
||||
"authorUrl": null,
|
||||
"authorAvatar": null,
|
||||
"insertedAt": "2024-08-15T11:33:04.823Z",
|
||||
"publishedAt": "2024-08-15T11:04:27.697Z",
|
||||
"media": [
|
||||
{
|
||||
"url": "https://img.alicdn.com/bao/uploaded/i4/2201501708471/O1CN01JbgDBP2CRm3TqFHQs_!!0-item_pic.jpg",
|
||||
"type": "photo",
|
||||
"width": 800,
|
||||
"height": 800
|
||||
},
|
||||
{
|
||||
"url": "https://img.alicdn.com/imgextra/i4/2201501708471/O1CN01rpvXMz2CRltEbTc4P_!!2201501708471.jpg",
|
||||
"type": "photo",
|
||||
"width": 1500,
|
||||
"height": 2322
|
||||
},
|
||||
{
|
||||
"url": "https://img.alicdn.com/imgextra/i4/2201501708471/O1CN01W0kow62CRlt2y0KYf_!!2201501708471.jpg",
|
||||
"type": "photo",
|
||||
"width": 1500,
|
||||
"height": 762
|
||||
},
|
||||
{
|
||||
"url": "https://img.alicdn.com/bao/uploaded/i4/2201501708471/O1CN01JbgDBP2CRm3TqFHQs_!!0-item_pic.jpg",
|
||||
"type": "photo",
|
||||
"width": 800,
|
||||
"height": 800
|
||||
}
|
||||
],
|
||||
"categories": null,
|
||||
"attachments": null
|
||||
},
|
||||
"feeds": {
|
||||
"id": "41397633064960000",
|
||||
"url": "https://www.ithome.com/rss/",
|
||||
"title": "IT 之家",
|
||||
"description": "IT 之家 - 软媒旗下网站",
|
||||
"siteUrl": "https://www.ithome.com/",
|
||||
"image": null,
|
||||
"checkedAt": "2024-08-15T11:33:04.823Z",
|
||||
"lastModifiedHeader": null,
|
||||
"etagHeader": null,
|
||||
"ttl": 30,
|
||||
"errorMessage": null,
|
||||
"errorAt": null,
|
||||
"ownerUserId": null
|
||||
},
|
||||
"collections": null,
|
||||
"settings": {}
|
||||
}
|
||||
]
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`test db cleaner > data should be set up correctly 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "feed-id-1",
|
||||
},
|
||||
{
|
||||
"id": "feed-id-2",
|
||||
},
|
||||
{
|
||||
"id": "feed-id-3",
|
||||
},
|
||||
{
|
||||
"id": "feed-id-4",
|
||||
},
|
||||
{
|
||||
"id": "feed-id-5",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test db cleaner > data should be set up correctly 2`] = `
|
||||
[
|
||||
{
|
||||
"feedId": "feed-id-1",
|
||||
"id": "entry-id-1",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-2",
|
||||
"id": "entry-id-2",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-3",
|
||||
"id": "entry-id-3",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-4",
|
||||
"id": "entry-id-4",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-5",
|
||||
"id": "entry-id-5",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-1",
|
||||
"id": "entry-id-6",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test db cleaner > data should be set up correctly 3`] = `
|
||||
[
|
||||
{
|
||||
"feedId": "feed-id-1",
|
||||
"id": "test-user-id-1/feed-id-1",
|
||||
"userId": "test-user-id-1",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-3",
|
||||
"id": "test-user-id-1/feed-id-3",
|
||||
"userId": "test-user-id-1",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-4",
|
||||
"id": "test-user-id-2/feed-id-4",
|
||||
"userId": "test-user-id-2",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-5",
|
||||
"id": "test-user-id-3/feed-id-5",
|
||||
"userId": "test-user-id-3",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-1",
|
||||
"id": "test-user-id/feed-id-1",
|
||||
"userId": "test-user-id",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-2",
|
||||
"id": "test-user-id/feed-id-2",
|
||||
"userId": "test-user-id",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import type Dexie from "dexie"
|
||||
import type { UpdateSpec } from "dexie"
|
||||
|
||||
export abstract class BaseService<T extends { id: string }> {
|
||||
constructor(public readonly table: Dexie.Table<T, string>) {}
|
||||
|
||||
async upsert(data: T): Promise<unknown> {
|
||||
return this.table.put(data)
|
||||
}
|
||||
|
||||
async upsertMany(data: T[]) {
|
||||
return this.table.bulkPut(data)
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.table.toArray()
|
||||
}
|
||||
|
||||
async patch(id: string, data: Partial<T>) {
|
||||
const oldData = await this.table.get(id)
|
||||
if (!oldData) return
|
||||
await this.table.update(id, {
|
||||
...oldData,
|
||||
...data,
|
||||
} as unknown as UpdateSpec<T>)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
// @ts-nocheck
|
||||
import type { EntryModel } from "@follow/models/types"
|
||||
import type { TargetModel } from "@follow/shared/hono"
|
||||
import { beforeAll, describe, expect, test } from "vitest"
|
||||
|
||||
import { browserDB } from "~/database"
|
||||
import type { SubscriptionFlatModel } from "~/store/subscription"
|
||||
|
||||
import { CleanerService } from "./cleaner"
|
||||
import { EntryService } from "./entry"
|
||||
import { FeedService } from "./feed"
|
||||
import { SubscriptionService } from "./subscription"
|
||||
|
||||
const currentUserID = "test-user-id"
|
||||
const otherUserIDs = ["test-user-id-1", "test-user-id-2", "test-user-id-3"]
|
||||
|
||||
const subscriptions: SubscriptionFlatModel[] = [
|
||||
{
|
||||
feedId: "feed-id-1",
|
||||
userId: currentUserID,
|
||||
},
|
||||
{
|
||||
feedId: "feed-id-2",
|
||||
userId: currentUserID,
|
||||
},
|
||||
{
|
||||
feedId: "feed-id-3",
|
||||
userId: otherUserIDs[0],
|
||||
},
|
||||
{
|
||||
feedId: "feed-id-4",
|
||||
userId: otherUserIDs[1],
|
||||
},
|
||||
{
|
||||
feedId: "feed-id-5",
|
||||
userId: otherUserIDs[2],
|
||||
},
|
||||
// ====
|
||||
{
|
||||
feedId: "feed-id-1",
|
||||
userId: otherUserIDs[0],
|
||||
},
|
||||
]
|
||||
|
||||
const feeds: TargetModel[] = [
|
||||
{
|
||||
id: "feed-id-1",
|
||||
},
|
||||
{
|
||||
id: "feed-id-2",
|
||||
},
|
||||
{
|
||||
id: "feed-id-3",
|
||||
},
|
||||
{
|
||||
id: "feed-id-4",
|
||||
},
|
||||
{
|
||||
id: "feed-id-5",
|
||||
},
|
||||
]
|
||||
|
||||
const entries: EntryModel[] = [
|
||||
{
|
||||
id: "entry-id-1",
|
||||
},
|
||||
{
|
||||
id: "entry-id-2",
|
||||
},
|
||||
{
|
||||
id: "entry-id-3",
|
||||
},
|
||||
{
|
||||
id: "entry-id-4",
|
||||
},
|
||||
{
|
||||
id: "entry-id-5",
|
||||
},
|
||||
{
|
||||
id: "entry-id-6",
|
||||
},
|
||||
]
|
||||
|
||||
const entryFeedIdMap = {
|
||||
"entry-id-1": "feed-id-1",
|
||||
"entry-id-2": "feed-id-2",
|
||||
"entry-id-3": "feed-id-3",
|
||||
"entry-id-4": "feed-id-4",
|
||||
"entry-id-5": "feed-id-5",
|
||||
"entry-id-6": "feed-id-1",
|
||||
}
|
||||
describe("test db cleaner", () => {
|
||||
beforeAll(async () => {
|
||||
await browserDB.delete()
|
||||
})
|
||||
beforeAll(async () => {
|
||||
await browserDB.open()
|
||||
await SubscriptionService.upsertMany(subscriptions)
|
||||
await FeedService.upsertMany(feeds)
|
||||
await EntryService.upsertMany(entries, entryFeedIdMap)
|
||||
})
|
||||
|
||||
test("data should be set up correctly", async () => {
|
||||
const feeds = await FeedService.findAll()
|
||||
const entries = await EntryService.findAll()
|
||||
const subscriptions = await SubscriptionService.findAll()
|
||||
expect(feeds).toMatchSnapshot()
|
||||
expect(entries).toMatchSnapshot()
|
||||
expect(subscriptions).toMatchSnapshot()
|
||||
})
|
||||
test("should clean remaining data", async () => {
|
||||
await CleanerService.cleanRemainingData(currentUserID)
|
||||
|
||||
const feeds = await FeedService.findAll()
|
||||
expect(feeds).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"id": "feed-id-1",
|
||||
},
|
||||
{
|
||||
"id": "feed-id-2",
|
||||
},
|
||||
]
|
||||
`)
|
||||
const subscripions = await SubscriptionService.findAll()
|
||||
expect(subscripions).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"feedId": "feed-id-1",
|
||||
"id": "test-user-id/feed-id-1",
|
||||
"userId": "test-user-id",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-2",
|
||||
"id": "test-user-id/feed-id-2",
|
||||
"userId": "test-user-id",
|
||||
},
|
||||
]
|
||||
`)
|
||||
const entries = await EntryService.findAll()
|
||||
expect(entries).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"feedId": "feed-id-1",
|
||||
"id": "entry-id-1",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-2",
|
||||
"id": "entry-id-2",
|
||||
},
|
||||
{
|
||||
"feedId": "feed-id-1",
|
||||
"id": "entry-id-6",
|
||||
},
|
||||
]
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import { browserDB } from "~/database"
|
||||
import type { CleanerType } from "~/database/schemas/cleaner"
|
||||
import { appLog } from "~/lib/log"
|
||||
|
||||
import { EntryService } from "./entry"
|
||||
import { FeedService } from "./feed"
|
||||
import { UnreadService } from "./feed-unread"
|
||||
import { InboxService } from "./inbox"
|
||||
import { ListService } from "./list"
|
||||
import { SubscriptionService } from "./subscription"
|
||||
|
||||
const cleanerModel = browserDB.cleaner
|
||||
class CleanerServiceStatic {
|
||||
// Clean other user subscriptions, should call this after login
|
||||
async cleanRemainingData(currentUserId: string) {
|
||||
const dbUserIds = await SubscriptionService.getUserIds()
|
||||
|
||||
const otherUserIds = dbUserIds.filter((id) => id !== currentUserId)
|
||||
const remainingSubscriptions = await SubscriptionService.getUserSubscriptions(otherUserIds)
|
||||
const currentSubscription = await SubscriptionService.getUserSubscriptions([currentUserId])
|
||||
const currentSubscriptionFeedsSet = new Set(currentSubscription.map((s) => s.feedId))
|
||||
// Finds a subscripion that does not exist for the current user, but a feedId that exists in the db
|
||||
|
||||
const toRemoveSubscription = remainingSubscriptions.filter(
|
||||
(s) => !currentSubscriptionFeedsSet.has(s.feedId),
|
||||
)
|
||||
|
||||
const toRemoveFeedIds = toRemoveSubscription.map((s) => s.feedId)
|
||||
|
||||
await Promise.allSettled([
|
||||
otherUserIds.map((id) => SubscriptionService.removeSubscription(id)),
|
||||
FeedService.bulkDelete(toRemoveFeedIds),
|
||||
UnreadService.bulkDelete(toRemoveFeedIds),
|
||||
EntryService.deleteEntriesByFeedIds(toRemoveFeedIds),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the which data recently used
|
||||
*/
|
||||
reset(list: { type: CleanerType; id: string }[]) {
|
||||
const now = Date.now()
|
||||
return cleanerModel.bulkPut(
|
||||
list.map((item) => ({
|
||||
refId: item.id,
|
||||
visitedAt: now,
|
||||
type: item.type,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the data that not used for a long time
|
||||
*/
|
||||
async cleanOutdatedData() {
|
||||
const now = Date.now()
|
||||
const expiredTime = now - 1000 * 60 * 60 * 24 * 30 // 30 days
|
||||
const data = await cleanerModel.where("visitedAt").below(expiredTime).toArray()
|
||||
|
||||
if (data.length === 0) {
|
||||
return
|
||||
}
|
||||
const deleteEntries = [] as string[]
|
||||
const deleteFeeds = [] as string[]
|
||||
const deleteLists = [] as string[]
|
||||
const deleteInboxes = [] as string[]
|
||||
for (const item of data) {
|
||||
switch (item.type) {
|
||||
case "feed": {
|
||||
deleteFeeds.push(item.refId)
|
||||
break
|
||||
}
|
||||
case "entry": {
|
||||
deleteEntries.push(item.refId)
|
||||
break
|
||||
}
|
||||
case "list": {
|
||||
deleteLists.push(item.refId)
|
||||
// TODO delete entries related to the list
|
||||
break
|
||||
}
|
||||
case "inbox": {
|
||||
deleteInboxes.push(item.refId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appLog("Clean outdated data...", "feeds:", deleteFeeds.length, "entries:", deleteEntries.length)
|
||||
await Promise.allSettled([
|
||||
FeedService.bulkDelete(deleteFeeds),
|
||||
EntryService.deleteEntries(deleteEntries),
|
||||
EntryService.deleteEntriesByFeedIds(deleteFeeds),
|
||||
cleanerModel.bulkDelete(data.map((d) => d.refId)),
|
||||
ListService.bulkDelete(deleteLists),
|
||||
InboxService.bulkDelete(deleteInboxes),
|
||||
])
|
||||
}
|
||||
|
||||
async cleanRefById(refIds: string[]) {
|
||||
return cleanerModel.bulkDelete(refIds)
|
||||
}
|
||||
}
|
||||
export const CleanerService = new CleanerServiceStatic()
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import { browserDB } from "~/database"
|
||||
|
||||
export enum EntryRelatedKey {
|
||||
READ = "READ",
|
||||
/** @deprecated */
|
||||
FEED_ID = "FEED_ID",
|
||||
COLLECTION = "COLLECTION",
|
||||
}
|
||||
|
||||
const taskQueue = new Map<EntryRelatedKey, Promise<any>>(
|
||||
[EntryRelatedKey.READ, EntryRelatedKey.FEED_ID, EntryRelatedKey.COLLECTION].map((key) => [
|
||||
key,
|
||||
Promise.resolve(),
|
||||
]),
|
||||
)
|
||||
|
||||
type IdToIdRecord = Record<string, string>
|
||||
type IdToBooleanRecord = Record<string, boolean>
|
||||
type IdToAnyObjectRecord = Record<string, Record<string, any>>
|
||||
const entryRelatedModel = browserDB.entryRelated
|
||||
class ServiceStatic {
|
||||
async findAll(type: EntryRelatedKey.FEED_ID): Promise<IdToIdRecord>
|
||||
async findAll(type: EntryRelatedKey.READ): Promise<IdToBooleanRecord>
|
||||
async findAll(type: EntryRelatedKey.COLLECTION): Promise<IdToAnyObjectRecord>
|
||||
|
||||
async findAll(type: EntryRelatedKey): Promise<Record<string, any>> {
|
||||
const data = await entryRelatedModel.get(type)
|
||||
return data ? data.data : {}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data key is entryId, value is read status
|
||||
* @returns
|
||||
*/
|
||||
async upsert(type: EntryRelatedKey.READ, data: IdToBooleanRecord): Promise<void>
|
||||
async upsert(type: EntryRelatedKey.FEED_ID, data: IdToIdRecord): Promise<void>
|
||||
async upsert(type: EntryRelatedKey.COLLECTION, data: IdToAnyObjectRecord): Promise<void>
|
||||
async upsert(type: any, data: Record<string, any>) {
|
||||
const getPreviousTask = taskQueue.get(type) || Promise.resolve()
|
||||
|
||||
const task = getPreviousTask.finally(async () => {
|
||||
const oldData = await this.findAll(type)
|
||||
|
||||
entryRelatedModel.put({
|
||||
data: { ...oldData, ...data },
|
||||
id: type,
|
||||
})
|
||||
})
|
||||
taskQueue.set(type, task)
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
async deleteItems(type: EntryRelatedKey, keys: string[]) {
|
||||
const oldData = await this.findAll(type as any)
|
||||
keys.forEach((key) => {
|
||||
delete oldData[key]
|
||||
})
|
||||
|
||||
return entryRelatedModel.put({
|
||||
data: oldData,
|
||||
id: type,
|
||||
})
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return entryRelatedModel.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export const EntryRelatedService = new ServiceStatic()
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import type { EntryModel } from "@follow/models/types"
|
||||
|
||||
import { browserDB } from "~/database"
|
||||
|
||||
import { BaseService } from "./base"
|
||||
import { CleanerService } from "./cleaner"
|
||||
import { EntryRelatedKey, EntryRelatedService } from "./entry-related"
|
||||
import type { Hydratable } from "./interface"
|
||||
|
||||
type EntryCollection = {
|
||||
createdAt: string
|
||||
}
|
||||
class EntryServiceStatic extends BaseService<EntryModel> implements Hydratable {
|
||||
constructor() {
|
||||
super(browserDB.entries)
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
override async upsertMany(data: EntryModel[], entryFeedMap: Record<string, string>) {
|
||||
const renewList = [] as { type: "entry"; id: string }[]
|
||||
const nextData = [] as (EntryModel & { feedId?: string; inboxId?: string })[]
|
||||
|
||||
for (const entry of data) {
|
||||
const feedId = entryFeedMap[entry.id]
|
||||
if (!feedId) {
|
||||
console.error("EntryService.upsertMany: feedId not found", entry)
|
||||
continue
|
||||
}
|
||||
renewList.push({ type: "entry", id: entry.id })
|
||||
nextData.push(Object.assign({}, entry, feedId ? { feedId } : { inboxId: feedId }))
|
||||
}
|
||||
|
||||
CleanerService.reset(renewList)
|
||||
|
||||
return super.upsertMany(nextData)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
override async upsert(feedId: string, data: EntryModel): Promise<unknown> {
|
||||
CleanerService.reset([
|
||||
{
|
||||
type: "entry",
|
||||
id: data.id,
|
||||
},
|
||||
])
|
||||
return super.upsert({
|
||||
...data,
|
||||
// @ts-expect-error
|
||||
feedId,
|
||||
})
|
||||
}
|
||||
|
||||
async bulkPatch(data: { key: string; changes: Partial<EntryModel> }[]) {
|
||||
await this.table.bulkUpdate(data)
|
||||
CleanerService.reset(data.map((d) => ({ type: "entry", id: d.key })))
|
||||
}
|
||||
|
||||
override async findAll() {
|
||||
return super.findAll() as Promise<(EntryModel & { feedId: string; inboxId: string })[]>
|
||||
}
|
||||
|
||||
bulkStoreReadStatus(record: Record<string, boolean>) {
|
||||
return EntryRelatedService.upsert(EntryRelatedKey.READ, record)
|
||||
}
|
||||
|
||||
async bulkStoreCollection(record: Record<string, EntryCollection>) {
|
||||
return EntryRelatedService.upsert(EntryRelatedKey.COLLECTION, record)
|
||||
}
|
||||
|
||||
async deleteCollection(entryId: string) {
|
||||
return EntryRelatedService.deleteItems(EntryRelatedKey.COLLECTION, [entryId])
|
||||
}
|
||||
|
||||
async deleteEntries(entryIds: string[]) {
|
||||
await Promise.all([
|
||||
this.table.bulkDelete(entryIds),
|
||||
EntryRelatedService.deleteItems(EntryRelatedKey.READ, entryIds),
|
||||
EntryRelatedService.deleteItems(EntryRelatedKey.COLLECTION, entryIds),
|
||||
CleanerService.cleanRefById(entryIds),
|
||||
])
|
||||
}
|
||||
|
||||
async deleteEntriesByFeedIds(feedIds: string[]) {
|
||||
const deleteEntryIds = await this.table.where("feedId").anyOf(feedIds).primaryKeys()
|
||||
await Promise.all([
|
||||
this.table.where("feedId").anyOf(feedIds).delete(),
|
||||
EntryRelatedService.deleteItems(EntryRelatedKey.READ, deleteEntryIds),
|
||||
EntryRelatedService.deleteItems(EntryRelatedKey.COLLECTION, deleteEntryIds),
|
||||
])
|
||||
}
|
||||
|
||||
async hydrate() {}
|
||||
}
|
||||
|
||||
export const EntryService = new EntryServiceStatic()
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { browserDB } from "~/database"
|
||||
|
||||
import type { Hydratable } from "./interface"
|
||||
|
||||
const unreadModel = browserDB.feedUnreads
|
||||
class ServiceStatic implements Hydratable {
|
||||
updateUnread(list: [string, number][]) {
|
||||
return unreadModel.bulkPut(list.map(([id, count]) => ({ id, count })))
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return unreadModel.toArray() as Promise<
|
||||
{
|
||||
id: string
|
||||
count: number
|
||||
}[]
|
||||
>
|
||||
}
|
||||
|
||||
clear() {
|
||||
return unreadModel.clear()
|
||||
}
|
||||
|
||||
async bulkDelete(ids: string[]) {
|
||||
return unreadModel.bulkDelete(ids)
|
||||
}
|
||||
|
||||
async hydrate() {}
|
||||
}
|
||||
|
||||
export const UnreadService = new ServiceStatic()
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import type { FeedModel, FeedOrListModel } from "@follow/models/types"
|
||||
|
||||
import { browserDB } from "~/database"
|
||||
|
||||
import { BaseService } from "./base"
|
||||
import { CleanerService } from "./cleaner"
|
||||
import type { Hydratable } from "./interface"
|
||||
|
||||
type FeedModelWithId = FeedModel & { id: string }
|
||||
class ServiceStatic extends BaseService<FeedModelWithId> implements Hydratable {
|
||||
constructor() {
|
||||
super(browserDB.feeds)
|
||||
}
|
||||
|
||||
override async upsertMany(data: FeedOrListModel[]) {
|
||||
const filterData = data.filter((d) => d.id)
|
||||
|
||||
CleanerService.reset(filterData.map((d) => ({ type: "feed", id: d.id! })))
|
||||
|
||||
return this.table.bulkPut(filterData as FeedModelWithId[])
|
||||
}
|
||||
|
||||
override async upsert(data: FeedOrListModel): Promise<string | null> {
|
||||
if (!data.id) return null
|
||||
CleanerService.reset([{ type: "feed", id: data.id }])
|
||||
return this.table.put(data as FeedModelWithId)
|
||||
}
|
||||
|
||||
async bulkDelete(ids: string[]) {
|
||||
return this.table.bulkDelete(ids)
|
||||
}
|
||||
|
||||
async hydrate() {}
|
||||
}
|
||||
|
||||
export const FeedService = new ServiceStatic()
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import type { InboxModel } from "@follow/models/types"
|
||||
|
||||
import { browserDB } from "~/database"
|
||||
|
||||
import { BaseService } from "./base"
|
||||
import { CleanerService } from "./cleaner"
|
||||
import type { Hydratable } from "./interface"
|
||||
|
||||
class ServiceStatic extends BaseService<{ id: string }> implements Hydratable {
|
||||
constructor() {
|
||||
super(browserDB.inboxes)
|
||||
}
|
||||
|
||||
override async upsertMany(data: InboxModel[]) {
|
||||
CleanerService.reset(data.map((d) => ({ type: "inbox", id: d.id! })))
|
||||
|
||||
return this.table.bulkPut(data)
|
||||
}
|
||||
|
||||
override async findAll() {
|
||||
return super.findAll() as unknown as InboxModel[]
|
||||
}
|
||||
|
||||
override async upsert(data: InboxModel): Promise<string | null> {
|
||||
if (!data.id) return null
|
||||
CleanerService.reset([{ type: "inbox", id: data.id }])
|
||||
return this.table.put(data)
|
||||
}
|
||||
|
||||
async bulkDelete(ids: string[]) {
|
||||
return this.table.bulkDelete(ids)
|
||||
}
|
||||
|
||||
async hydrate() {}
|
||||
}
|
||||
|
||||
export const InboxService = new ServiceStatic()
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export * from "./entry"
|
||||
export * from "./entry-related"
|
||||
export * from "./feed"
|
||||
export * from "./feed-unread"
|
||||
export * from "./subscription"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export interface Hydratable {
|
||||
hydrate: () => Promise<void>
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import type { ListModel } from "@follow/models/types"
|
||||
|
||||
import { browserDB } from "~/database"
|
||||
|
||||
import { BaseService } from "./base"
|
||||
import { CleanerService } from "./cleaner"
|
||||
import type { Hydratable } from "./interface"
|
||||
|
||||
class ServiceStatic extends BaseService<{ id: string }> implements Hydratable {
|
||||
constructor() {
|
||||
super(browserDB.lists)
|
||||
}
|
||||
|
||||
override async upsertMany(data: ListModel[]) {
|
||||
CleanerService.reset(data.map((d) => ({ type: "list", id: d.id! })))
|
||||
|
||||
// FIXME The backend should not pass these computed attributes, and these need to be removed here.
|
||||
// Subsequent refactoring of the backend data flow should not nest computed attributes
|
||||
return this.table.bulkPut(data.map(({ owner, ...d }) => d) as ListModel[])
|
||||
}
|
||||
|
||||
override async findAll() {
|
||||
return super.findAll() as unknown as ListModel[]
|
||||
}
|
||||
|
||||
override async upsert({ owner, ...data }: ListModel): Promise<string | null> {
|
||||
if (!data.id) return null
|
||||
CleanerService.reset([{ type: "list", id: data.id }])
|
||||
return this.table.put(data as ListModel)
|
||||
}
|
||||
|
||||
async bulkDelete(ids: string[]) {
|
||||
return this.table.bulkDelete(ids)
|
||||
}
|
||||
|
||||
async findAndUpdate(id: string, data: Partial<ListModel>) {
|
||||
const list = await this.table.get(id)
|
||||
if (!list) return
|
||||
return this.table.put({ ...list, ...data })
|
||||
}
|
||||
|
||||
async hydrate() {}
|
||||
}
|
||||
|
||||
export const ListService = new ServiceStatic()
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import { uniq } from "es-toolkit/compat"
|
||||
|
||||
import { browserDB } from "~/database"
|
||||
|
||||
import { BaseService } from "./base"
|
||||
import type { Hydratable } from "./interface"
|
||||
|
||||
type SubscriptionModelWithId = any & { id: string }
|
||||
|
||||
class SubscriptionServiceStatic extends BaseService<SubscriptionModelWithId> implements Hydratable {
|
||||
constructor() {
|
||||
super(browserDB.subscriptions)
|
||||
}
|
||||
|
||||
public getUserSubscriptions(userIds: string[]) {
|
||||
return this.table.where("userId").anyOf(userIds).toArray()
|
||||
}
|
||||
|
||||
public async getUserIds() {
|
||||
return uniq(
|
||||
(await this.table
|
||||
.toCollection()
|
||||
.uniqueKeys()
|
||||
.then((keys) => keys.map((k) => k.toString().split("/")[0]))) as string[],
|
||||
)
|
||||
}
|
||||
|
||||
override async upsertMany(data: any[]) {
|
||||
return this.table.bulkPut(
|
||||
data.map(({ feeds, lists, inboxes, ...d }: any) => ({
|
||||
...d,
|
||||
id: this.uniqueId(d.userId, d.feedId),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
override upsert(data: any) {
|
||||
return this.table.put({
|
||||
...data,
|
||||
id: this.uniqueId(data.userId, data.feedId),
|
||||
})
|
||||
}
|
||||
|
||||
private uniqueId(userId: string, feedId: string) {
|
||||
return `${userId}/${feedId}`
|
||||
}
|
||||
|
||||
async changeView(feedId: string, view: number) {
|
||||
return this.table.where("feedId").equals(feedId).modify({ view })
|
||||
}
|
||||
async changeViews(feedIdList: string[], view: number) {
|
||||
return this.table.where("feedId").anyOf(feedIdList).modify({ view })
|
||||
}
|
||||
|
||||
async updateCategory(feedId: string, category?: string | null) {
|
||||
return this.table.where("feedId").equals(feedId).modify({ category })
|
||||
}
|
||||
async updateCategories(feedIdList: string[], category?: string | null) {
|
||||
return this.table.where("feedId").anyOf(feedIdList).modify({ category })
|
||||
}
|
||||
|
||||
async removeSubscription(userId: string, feedId: string): Promise<void>
|
||||
// @ts-expect-error
|
||||
async removeSubscription(userId: string): Promise<void>
|
||||
async removeSubscription(userId: string, feedId: string) {
|
||||
if (feedId && userId) {
|
||||
return this.table.delete(this.uniqueId(userId, feedId))
|
||||
}
|
||||
if (!feedId && userId) {
|
||||
return this.table.where("userId").equals(userId).delete()
|
||||
}
|
||||
}
|
||||
|
||||
async removeSubscriptionMany(userId: string, feedIdList: string[]) {
|
||||
return this.table.bulkDelete(feedIdList.map((feedId) => this.uniqueId(userId, feedId)))
|
||||
}
|
||||
|
||||
async renameCategory(userId: string, feedIdList: string[], category: string) {
|
||||
return this.table
|
||||
.where("userId")
|
||||
.equals(userId)
|
||||
.and((item) => feedIdList.includes(item.feedId))
|
||||
.modify({ category })
|
||||
}
|
||||
|
||||
async hydrate() {}
|
||||
}
|
||||
|
||||
export const SubscriptionService = new SubscriptionServiceStatic()
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
import type { EntryModel } from "@follow/models/types"
|
||||
import { EntryService } from "@follow/database/services/entry"
|
||||
import { FeedService } from "@follow/database/services/feed"
|
||||
import { SubscriptionService } from "@follow/database/services/subscription"
|
||||
import type { EntryModel } from "@follow/store/entry/types"
|
||||
import type { SubscriptionModel } from "@follow/store/subscription/types"
|
||||
import { getStorageNS } from "@follow/utils/ns"
|
||||
import type { IFuseOptions } from "fuse.js"
|
||||
|
|
@ -7,7 +10,6 @@ import { useAtomValue } from "jotai"
|
|||
import { atomWithStorage } from "jotai/utils"
|
||||
|
||||
import { jotaiStore } from "~/lib/jotai"
|
||||
import { EntryService, FeedService, SubscriptionService } from "~/services"
|
||||
|
||||
import { createZustandStore } from "../utils/helper"
|
||||
import { SearchType } from "./constants"
|
||||
|
|
@ -45,9 +47,12 @@ class SearchActions {
|
|||
|
||||
async createLocalDbSearch() {
|
||||
const [entries, feeds, subscriptions] = await Promise.all([
|
||||
EntryService.findAll(),
|
||||
FeedService.findAll(),
|
||||
SubscriptionService.findAll(),
|
||||
EntryService.getEntryAll(),
|
||||
(await FeedService.getFeedAll()).map((feed) => ({
|
||||
...feed,
|
||||
type: "feed" as const,
|
||||
})),
|
||||
SubscriptionService.getSubscriptionAll(),
|
||||
])
|
||||
|
||||
const feedsMap = new Map(feeds.map((feed) => [feed.id, feed]))
|
||||
|
|
@ -72,7 +77,7 @@ class SearchActions {
|
|||
|
||||
const processedEntries = [] as SearchResult<EntryModel, { feedId: string }>[]
|
||||
for (const entry of entries) {
|
||||
const feedId = feedsMap.get(entry.item.feedId)?.id
|
||||
const feedId = entry.item.feedId ? feedsMap.get(entry.item.feedId)?.id : undefined
|
||||
if (feedId) {
|
||||
processedEntries.push({ item: entry.item, feedId })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { EntryModel, FeedOrListRespModel } from "@follow/models/types"
|
||||
import type { EntryModel } from "@follow/store/entry/types"
|
||||
import type { FeedModel } from "@follow/store/feed/types"
|
||||
import type { SubscriptionModel } from "@follow/store/subscription/types"
|
||||
|
||||
// @ts-expect-error
|
||||
|
|
@ -7,7 +8,7 @@ export interface SearchResult<T extends object, A extends object = object> exten
|
|||
}
|
||||
|
||||
export interface SearchState {
|
||||
feeds: SearchResult<FeedOrListRespModel>[]
|
||||
feeds: SearchResult<FeedModel>[]
|
||||
entries: SearchResult<EntryModel, { feedId: string }>[]
|
||||
subscriptions: SearchResult<SubscriptionModel, { feedId: string }>[]
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { unreadActions } from "@follow/store/unread/store"
|
|||
import { getStorageNS } from "@follow/utils/ns"
|
||||
|
||||
import { clearUISettings } from "~/atoms/settings/ui"
|
||||
import { browserDB } from "~/database"
|
||||
|
||||
import { clearImageDimensionsDb } from "../image/db"
|
||||
|
||||
|
|
@ -27,7 +26,6 @@ export const clearLocalPersistStoreData = async () => {
|
|||
clearUISettings()
|
||||
|
||||
await clearImageDimensionsDb()
|
||||
await Promise.all(browserDB.tables.map((table) => table.clear()))
|
||||
}
|
||||
|
||||
const storedUserId = getStorageNS("user_id")
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import { shallow } from "zustand/shallow"
|
|||
import type { UseBoundStoreWithEqualityFn } from "zustand/traditional"
|
||||
import { createWithEqualityFn } from "zustand/traditional"
|
||||
|
||||
import { runTransactionInScope } from "~/database"
|
||||
|
||||
declare const window: any
|
||||
export const localStorage: PersistStorage<any> = {
|
||||
getItem: (name: string) => {
|
||||
|
|
@ -172,12 +170,10 @@ class Transaction<S, Ctx> {
|
|||
}
|
||||
|
||||
if (this.onPersist) {
|
||||
await runTransactionInScope(() =>
|
||||
Promise.resolve(this.onPersist!(this._snapshot, this._ctx)).catch((err) => {
|
||||
console.error(err)
|
||||
throw err
|
||||
}),
|
||||
)
|
||||
await Promise.resolve(this.onPersist!(this._snapshot, this._ctx)).catch((err) => {
|
||||
console.error(err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ export default ({ mode }) => {
|
|||
["react", "react-dom"],
|
||||
["react-error-boundary", "react-dom/server", "react-router"],
|
||||
// Data Statement
|
||||
["zustand", "jotai", "use-context-selector", "immer", "dexie"],
|
||||
["zustand", "jotai", "use-context-selector", "immer"],
|
||||
// Remark
|
||||
[
|
||||
"remark-directive",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const UserProvider = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!session?.user) return
|
||||
// @ts-expect-error FIXME
|
||||
setWhoami(session.user)
|
||||
|
||||
setIntegrationIdentify(session.user)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const UserProvider = () => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!session?.user) return
|
||||
// @ts-expect-error FIXME
|
||||
setWhoami(session.user)
|
||||
|
||||
setIntegrationIdentify(session.user)
|
||||
|
|
|
|||
|
|
@ -195,8 +195,8 @@ class UnreadSyncService {
|
|||
}
|
||||
})
|
||||
|
||||
tx.request(() => {
|
||||
return apiClient().reads.$post({
|
||||
tx.request(async () => {
|
||||
await apiClient().reads.$post({
|
||||
json: { entryIds: [entryId] },
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -550,9 +550,6 @@ importers:
|
|||
dayjs:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
dexie:
|
||||
specifier: 4.0.11
|
||||
version: 4.0.11
|
||||
dnum:
|
||||
specifier: 2.15.0
|
||||
version: 2.15.0
|
||||
|
|
@ -8072,9 +8069,6 @@ packages:
|
|||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
dexie@4.0.11:
|
||||
resolution: {integrity: sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==}
|
||||
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
|
|
@ -24208,8 +24202,6 @@ snapshots:
|
|||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
dexie@4.0.11: {}
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
diff@4.0.2: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue