pref: server end

This commit is contained in:
Ou 2024-10-05 17:20:49 +08:00
parent 9389f23ad7
commit 8ca82048de
9 changed files with 94 additions and 94 deletions

View File

@ -1,5 +1,4 @@
import { TTL } from "@shared/consts" import type { CacheInfo, NewsItem } from "@shared/types"
import type { CacheInfo } from "@shared/types"
import type { Database } from "db0" import type { Database } from "db0"
export class Cache { export class Cache {
@ -13,26 +12,25 @@ export class Cache {
await this.db.prepare(` await this.db.prepare(`
CREATE TABLE IF NOT EXISTS cache ( CREATE TABLE IF NOT EXISTS cache (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
data TEXT,
updated INTEGER, updated INTEGER,
expires INTEGER data TEXT
); );
`).run() `).run()
console.log(`init: `, performance.now() - last) console.log(`init: `, performance.now() - last)
} }
async set(key: string, value: any) { async set(key: string, value: NewsItem[]) {
const now = Date.now() const now = Date.now()
const last = performance.now() const last = performance.now()
await this.db.prepare( await this.db.prepare(
`INSERT OR REPLACE INTO cache (id, data, updated, expires) VALUES (?, ?, ?, ?)`, `INSERT OR REPLACE INTO cache (id, data, updated) VALUES (?, ?, ?)`,
).run(key, JSON.stringify(value), now, now + TTL) ).run(key, JSON.stringify(value), now)
console.log(`set ${key}: `, performance.now() - last) console.log(`set ${key}: `, performance.now() - last)
} }
async get(key: string): Promise<CacheInfo> { async get(key: string): Promise<CacheInfo> {
const last = performance.now() const last = performance.now()
const row: any = await this.db.prepare(`SELECT id, data, updated, expires FROM cache WHERE id = ?`).get(key) const row: any = await this.db.prepare(`SELECT id, data, updated FROM cache WHERE id = ?`).get(key)
const r = row const r = row
? { ? {
...row, ...row,

View File

@ -1,50 +1,70 @@
import { fallback, sources } from "#/sources" import { Interval, TTL } from "@shared/consts"
import type { SourceResponse } from "@shared/types"
import { sources } from "@shared/data"
import { fallback, sourcesFn } from "#/sources"
import { Cache } from "#/cache" import { Cache } from "#/cache"
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event): Promise<SourceResponse> => {
try { try {
const id = getRouterParam(event, "id") as keyof typeof sources const id = getRouterParam(event, "id") as keyof typeof sourcesFn
const query = getQuery(event) const query = getQuery(event)
const latest = query.latest !== undefined && query.latest !== "false" const latest = query.latest !== undefined && query.latest !== "false"
if (!id) throw new Error("Invalid source id") if (!id || !sources[id]) throw new Error("Invalid source id")
const db = useDatabase() const db = useDatabase()
const cacheStore = db ? new Cache(db) : undefined const cacheStore = db ? new Cache(db) : undefined
const now = Date.now()
if (cacheStore) { if (cacheStore) {
// await cacheStore.init() // await cacheStore.init()
const cache = await cacheStore.get(id) const cache = await cacheStore.get(id)
if (cache) { if (cache) {
if (!latest && cache.expires > Date.now()) { if (!latest && now - cache.updated < TTL) {
return { return {
status: "cache", status: "cache",
data: cache.data, data: {
updatedTime: cache.updated,
items: cache.data,
},
} }
} else if (latest && Date.now() - cache.updated < 60 * 1000) { } else if (latest) {
let interval = Interval
if ("interval" in sources[id]) interval = sources[id].interval as number
if (now - cache.updated < interval) {
return { return {
status: "success", status: "success",
data: cache.data, data: {
updatedTime: now,
items: cache.data,
},
}
} }
} }
} }
} }
if (!sources[id]) { if (sourcesFn[id]) {
const last = performance.now() const last = performance.now()
const data = await fallback(id) const data = await sourcesFn[id]()
console.log(`fetch: ${id}`, performance.now() - last) console.log(`fetch: ${id}`, performance.now() - last)
if (cacheStore) await cacheStore.set(id, data) if (cacheStore) event.waitUntil(cacheStore.set(id, data))
return { return {
status: "success", status: "success",
data, data: {
updatedTime: now,
items: data,
},
} }
} else { } else {
const last = performance.now() const last = performance.now()
const data = await (await sources[id])() const data = await fallback(id)
console.log(`fetch: ${id}`, performance.now() - last) console.log(`fetch: ${id}`, performance.now() - last)
if (cacheStore) await cacheStore.set(id, data) if (cacheStore) event.waitUntil(cacheStore.set(id, data))
return { return {
status: "success", status: "success",
data, data: {
updatedTime: now,
items: data,
},
} }
} }
} catch (e: any) { } catch (e: any) {

View File

@ -1,4 +1,4 @@
import type { SourceInfo } from "@shared/types" import type { NewsItem } from "@shared/types"
export interface Res { export interface Res {
code: number code: number
@ -17,13 +17,11 @@ export interface Res {
}[] }[]
} }
export async function fallback(id: string): Promise<SourceInfo> { export async function fallback(id: string): Promise<NewsItem[]> {
const url = `https://smzdk.top/api/${id}/new` const url = `https://smzdk.top/api/${id}/new`
const res: Res = await $fetch(url) const res: Res = await $fetch(url)
if (res.code !== 200 || !res.data) throw new Error(res.message) if (res.code !== 200 || !res.data) throw new Error(res.message)
return { return res.data.map(item => ({
updatedTime: res.updateTime,
items: res.data.map(item => ({
extra: { extra: {
date: item.time, date: item.time,
}, },
@ -31,6 +29,5 @@ export async function fallback(id: string): Promise<SourceInfo> {
title: item.title, title: item.title,
url: item.url, url: item.url,
mobileUrl: item.mobileUrl, mobileUrl: item.mobileUrl,
})), }))
}
} }

View File

@ -1,4 +1,4 @@
import type { SourceID, SourceInfo } from "@shared/types" import type { NewsItem, SourceID } from "@shared/types"
import peopledaily from "./peopledaily" import peopledaily from "./peopledaily"
import weibo from "./weibo" import weibo from "./weibo"
import zaobao from "./zaobao" import zaobao from "./zaobao"
@ -8,11 +8,11 @@ import wallstreetcn from "./wallstreetcn"
export { fallback } from "./fallback" export { fallback } from "./fallback"
export const sources = { export const sourcesFn = {
peopledaily, peopledaily,
weibo, weibo,
zaobao, zaobao,
wallstreetcn, wallstreetcn,
"36kr-quick": krQ, "36kr-quick": krQ,
// "36kr": kr, // "36kr": kr,
} as Record<SourceID, () => Promise<SourceInfo>> } as Record<SourceID, () => Promise<NewsItem[]>>

View File

@ -1,28 +1,22 @@
import { RSSHubBase } from "@shared/consts" import { RSSHubBase } from "@shared/consts"
import type { NewsItem, RSSHubInfo, SourceInfo } from "@shared/types" import type { NewsItem, RSSHubInfo } from "@shared/types"
export function defineSource(source: () => Promise<NewsItem[]>): () => Promise<SourceInfo> { export function defineSource(source: () => Promise<NewsItem[]>): () => Promise<NewsItem[]> {
return async () => ({ return source
updatedTime: Date.now(),
items: await source(),
})
} }
export function defineRSSSource(url: string): () => Promise<SourceInfo> { export function defineRSSSource(url: string): () => Promise<NewsItem[]> {
return async () => { return async () => {
const data = await rss2json(url) const data = await rss2json(url)
if (!data?.items.length) throw new Error("Cannot fetch data") if (!data?.items.length) throw new Error("Cannot fetch data")
return { return data.items.slice(0, 20).map(item => ({
updatedTime: data.updatedTime ?? Date.now(),
items: data.items.slice(0, 20).map(item => ({
title: item.title, title: item.title,
url: item.link, url: item.link,
id: item.link, id: item.link,
extra: { extra: {
date: item.created, date: item.created,
}, },
})), }))
}
} }
} }
@ -32,7 +26,7 @@ interface Option {
// default: 20 // default: 20
limit?: number limit?: number
} }
export function defineRSSHubSource(route: string, option?: Option): () => Promise<SourceInfo> { export function defineRSSHubSource(route: string, option?: Option): () => Promise<NewsItem[]> {
return async () => { return async () => {
const url = new URL(route, RSSHubBase) const url = new URL(route, RSSHubBase)
url.searchParams.set("format", "json") url.searchParams.set("format", "json")
@ -45,16 +39,13 @@ export function defineRSSHubSource(route: string, option?: Option): () => Promis
url.searchParams.set(key, value.toString()) url.searchParams.set(key, value.toString())
}) })
const data: RSSHubInfo = await $fetch(url) const data: RSSHubInfo = await $fetch(url)
return { return data.items.slice(0, 20).map(item => ({
updatedTime: Date.now(),
items: data.items.slice(0, 20).map(item => ({
title: item.title, title: item.title,
url: item.url, url: item.url,
id: item.id ?? item.url, id: item.id ?? item.url,
extra: { extra: {
date: item.date_published, date: item.date_published,
}, },
})), }))
}
} }
} }

View File

@ -1,8 +1,11 @@
export const TTL = 15 * 60 * 1000
/** /**
* *
*/ */
export const Interval = 30 * 60 * 1000 export const TTL = 30 * 60 * 1000
/**
*
*/
export const Interval = 10 * 60 * 1000
export const RSSHubBase = "https://rsshub.rssforever.com" export const RSSHubBase = "https://rsshub.rssforever.com"
// export const RSSHubBase = "https://rsshub.pseudoyu.com" // export const RSSHubBase = "https://rsshub.pseudoyu.com"

View File

@ -5,8 +5,8 @@ export const sectionIds = ["focus", "social", "china", "world", "digital"] as co
export const sources = { export const sources = {
"wallstreetcn": { "wallstreetcn": {
name: "华尔街见闻", name: "华尔街见闻",
interval: 10,
home: "https://wallstreetcn.com/", home: "https://wallstreetcn.com/",
interval: 3 * 60 * 1000,
type: "快讯", type: "快讯",
}, },
// "36kr": { // "36kr": {
@ -18,12 +18,12 @@ export const sources = {
"36kr-quick": { "36kr-quick": {
name: "36氪", name: "36氪",
type: "快讯", type: "快讯",
interval: 3, interval: 3 * 60 * 1000,
home: "https://36kr.com", home: "https://36kr.com",
}, },
"douyin": { "douyin": {
name: "抖音", name: "抖音",
interval: 1, interval: 3 * 60 * 1000,
home: "https://www.douyin.com", home: "https://www.douyin.com",
}, },
"hupu": { "hupu": {
@ -32,24 +32,21 @@ export const sources = {
}, },
"zhihu": { "zhihu": {
name: "知乎", name: "知乎",
interval: 10,
home: "https://www.zhihu.com", home: "https://www.zhihu.com",
}, },
"weibo": { "weibo": {
name: "微博", name: "微博",
type: "实时热搜", type: "实时热搜",
interval: 1, interval: 1 * 60 * 1000,
home: "https://weibo.com", home: "https://weibo.com",
}, },
"tieba": { "tieba": {
name: "百度贴吧", name: "百度贴吧",
interval: 2,
home: "https://tieba.baidu.com", home: "https://tieba.baidu.com",
}, },
"zaobao": { "zaobao": {
name: "联合早报", name: "联合早报",
type: "实时新闻", type: "实时新闻",
interval: 10,
home: "https://www.zaobao.com", home: "https://www.zaobao.com",
}, },
"thepaper": { "thepaper": {
@ -58,7 +55,6 @@ export const sources = {
}, },
"toutiao": { "toutiao": {
name: "今日头条", name: "今日头条",
interval: 2,
home: "https://www.toutiao.com", home: "https://www.toutiao.com",
}, },
"cankaoxiaoxi": { "cankaoxiaoxi": {
@ -73,13 +69,9 @@ export const sources = {
name: string name: string
type?: string type?: string
/** /**
* *
*/ */
interval?: number interval?: number
/**
*
*/
once?: number
home: string home: string
}> }>

View File

@ -23,7 +23,7 @@ export interface SourceInfo {
items: NewsItem[] items: NewsItem[]
} }
export type OResponse = { export type SourceResponse = {
status: "success" | "cache" status: "success" | "cache"
data: SourceInfo data: SourceInfo
} | { } | {
@ -48,9 +48,8 @@ export interface RSSItem {
export interface CacheInfo { export interface CacheInfo {
id: SourceID id: SourceID
data: SourceInfo data: NewsItem[]
updated: number updated: number
expires: number
} }
export interface RSSHubInfo { export interface RSSHubInfo {

View File

@ -1,4 +1,4 @@
import type { NewsItem, SourceID, SourceInfo } from "@shared/types" import type { NewsItem, SourceID, SourceInfo, SourceResponse } from "@shared/types"
import { OverlayScrollbarsComponent } from "overlayscrollbars-react" import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
import type { UseQueryResult } from "@tanstack/react-query" import type { UseQueryResult } from "@tanstack/react-query"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
@ -71,11 +71,11 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
if (Date.now() - _refetchTime < 1000) { if (Date.now() - _refetchTime < 1000) {
url = `/api/${_id}?latest` url = `/api/${_id}?latest`
} }
const response = await fetch(url).then(res => res.json()) const response: SourceResponse = await fetch(url).then(res => res.json())
if (response.status === "error") { if (response.status === "error") {
throw new Error(response.message) throw new Error(response.message)
} else { } else {
return response.data as SourceInfo return response.data
} }
}, },
// refetch 时显示原有的数据 // refetch 时显示原有的数据