webapp/src/context/topics.tsx

197 lines
5.4 KiB
TypeScript
Raw Normal View History

2024-05-06 23:44:25 +00:00
import { createLazyMemo } from '@solid-primitives/memo'
2024-03-18 11:07:28 +00:00
import { openDB } from 'idb'
2024-06-24 17:50:27 +00:00
import {
Accessor,
JSX,
createContext,
createEffect,
createMemo,
createSignal,
on,
onMount,
useContext,
} from 'solid-js'
import { loadTopics } from '~/lib/api'
import { getRandomTopicsFromArray } from '~/utils/getRandomTopicsFromArray'
2024-03-18 11:07:28 +00:00
import { Topic } from '../graphql/schema/core.gen'
2024-05-06 23:44:25 +00:00
import { byTopicStatDesc } from '../utils/sortby'
2024-03-18 11:07:28 +00:00
type TopicsContextType = {
2024-05-06 23:44:25 +00:00
topicEntities: Accessor<{ [topicSlug: string]: Topic }>
sortedTopics: Accessor<Topic[]>
2024-06-24 17:50:27 +00:00
randomTopic: Accessor<Topic | undefined>
2024-05-06 23:44:25 +00:00
topTopics: Accessor<Topic[]>
setTopicsSort: (sortBy: string) => void
addTopics: (topics: Topic[]) => void
2024-05-07 00:05:04 +00:00
loadTopics: () => Promise<Topic[]>
2024-03-18 11:07:28 +00:00
}
2024-06-24 17:50:27 +00:00
const TopicsContext = createContext<TopicsContextType>({
topicEntities: () => ({}) as Record<string, Topic>,
sortedTopics: () => [] as Topic[],
topTopics: () => [] as Topic[],
setTopicsSort: (_s: string) => undefined,
addTopics: (_ttt: Topic[]) => undefined,
loadTopics: async () => [] as Topic[],
} as TopicsContextType)
2024-03-18 11:07:28 +00:00
export function useTopics() {
return useContext(TopicsContext)
}
const DB_NAME = 'discourseAppDB'
const DB_VERSION = 1
const STORE_NAME = 'topics'
2024-06-24 17:50:27 +00:00
const CACHE_LIFETIME = 24 * 60 * 60 * 1000 // один день в миллисекундах
2024-03-18 11:07:28 +00:00
const setupIndexedDB = async () => {
return await openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' })
}
},
})
}
2024-06-24 17:50:27 +00:00
const getTopicsFromIndexedDB = async (db: IDBDatabase) => {
if (db) {
return new Promise<{ topics: Topic[]; timestamp: number }>((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly')
const store = tx.objectStore(STORE_NAME)
const request = store.getAll()
request.onsuccess = () => {
const topics = request.result || []
const timestamp =
(tx.objectStore(STORE_NAME).get('timestamp') as IDBRequest<{ value: number }>).result?.value || 0
resolve({ topics, timestamp })
}
request.onerror = () => {
console.error('Error fetching topics from IndexedDB')
reject()
}
})
}
return { topics: [], timestamp: 0 }
2024-03-18 11:07:28 +00:00
}
2024-05-07 00:05:04 +00:00
2024-06-24 17:50:27 +00:00
const saveTopicsToIndexedDB = async (db: IDBDatabase, topics: Topic[]) => {
if (db) {
const tx = (db as IDBDatabase).transaction(STORE_NAME, 'readwrite')
const store = tx.objectStore(STORE_NAME)
const timestamp = Date.now()
topics?.forEach(async (topic: Topic) => {
if (topic) await store.put(topic as Topic)
})
await store.put({ id: 'timestamp', value: timestamp })
// @ts-ignore
await tx.done
2024-03-18 11:07:28 +00:00
}
}
export const TopicsProvider = (props: { children: JSX.Element }) => {
2024-05-06 23:44:25 +00:00
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
const [sortAllBy, setSortAllBy] = createSignal<'shouts' | 'followers' | 'authors' | 'title'>('shouts')
2024-03-18 11:07:28 +00:00
2024-05-06 23:44:25 +00:00
const sortedTopics = createLazyMemo<Topic[]>(() => {
const topics = Object.values(topicEntities())
switch (sortAllBy()) {
case 'followers': {
topics.sort(byTopicStatDesc('followers'))
break
}
case 'shouts': {
topics.sort(byTopicStatDesc('shouts'))
break
}
case 'authors': {
topics.sort(byTopicStatDesc('authors'))
break
}
case 'title': {
2024-06-24 17:50:27 +00:00
topics.sort((a, b) => (a?.title || '').localeCompare(b?.title || ''))
2024-05-06 23:44:25 +00:00
break
}
default: {
topics.sort(byTopicStatDesc('shouts'))
}
}
return topics
})
const topTopics = createMemo(() => {
const topics = Object.values(topicEntities())
topics.sort(byTopicStatDesc('shouts'))
return topics
})
const addTopics = (...args: Topic[][]) => {
const allTopics = args.flatMap((topics) => (topics || []).filter(Boolean))
const newTopicEntities = allTopics.reduce(
(acc, topic) => {
acc[topic.slug] = topic
return acc
},
{} as Record<string, Topic>,
)
setTopicEntities((prevTopicEntities) => {
return {
...prevTopicEntities,
...newTopicEntities,
}
})
}
2024-06-24 17:50:27 +00:00
2024-05-07 00:05:04 +00:00
const [db, setDb] = createSignal()
2024-06-24 17:50:27 +00:00
const loadAllTopics = async () => {
const topicsLoader = loadTopics()
const ttt = await topicsLoader()
if (db()) await saveTopicsToIndexedDB(db() as IDBDatabase, ttt as Topic[])
return ttt || []
2024-05-07 00:05:04 +00:00
}
2024-05-06 23:44:25 +00:00
2024-03-18 11:07:28 +00:00
onMount(async () => {
2024-06-24 17:50:27 +00:00
setDb(await setupIndexedDB())
console.info('[context.topics] idb loaded')
2024-03-18 11:07:28 +00:00
})
2024-06-24 17:50:27 +00:00
const [randomTopic, setRandomTopic] = createSignal<Topic>()
createEffect(
on(
db,
async (indexed) => {
if (indexed) {
const { topics: req, timestamp } = await getTopicsFromIndexedDB(indexed as IDBDatabase)
const now = Date.now()
const isCacheValid = now - timestamp < CACHE_LIFETIME
const topics = isCacheValid ? req : await loadAllTopics()
console.info(`[context.topics] got ${(topics as Topic[]).length || 0} topics`)
addTopics(topics as Topic[])
setRandomTopic(getRandomTopicsFromArray(topics || [], 1).pop())
}
},
{ defer: true },
),
)
2024-05-06 23:44:25 +00:00
const value: TopicsContextType = {
setTopicsSort: setSortAllBy,
topicEntities,
sortedTopics,
2024-06-24 17:50:27 +00:00
randomTopic,
2024-05-06 23:44:25 +00:00
topTopics,
addTopics,
2024-06-24 17:50:27 +00:00
loadTopics: loadAllTopics,
2024-05-06 23:44:25 +00:00
}
2024-03-18 11:07:28 +00:00
return <TopicsContext.Provider value={value}>{props.children}</TopicsContext.Provider>
}