webapp/src/context/topics.tsx

234 lines
6.6 KiB
TypeScript
Raw Normal View History

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,
2024-07-18 11:22:59 +00:00
onMount,
2024-06-26 08:22:05 +00:00
useContext
2024-06-24 17:50:27 +00:00
} from 'solid-js'
import { loadTopics } from '~/graphql/api/public'
import { Topic } from '~/graphql/schema/core.gen'
2024-08-30 13:45:17 +00:00
import { getRandomItemsFromArray } from '~/utils/random'
import { byTopicStatDesc } from '../utils/sort'
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,
2024-07-13 16:29:17 +00:00
loadTopics: async () => [] as Topic[],
randomTopic: () => undefined
2024-06-24 17:50:27 +00:00
} 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 () => {
2024-07-06 06:41:16 +00:00
if (window && !('indexedDB' in window)) {
2024-06-28 11:45:25 +00:00
console.error("This browser doesn't support IndexedDB")
return
}
try {
const db = await openDB(DB_NAME, DB_VERSION, {
upgrade(db, oldVersion, newVersion, _transaction) {
console.log(`Upgrading database from version ${oldVersion} to ${newVersion}`)
if (db.objectStoreNames.contains(STORE_NAME)) {
console.log(`Object store ${STORE_NAME} already exists`)
} else {
console.log(`Creating object store: ${STORE_NAME}`)
db.createObjectStore(STORE_NAME, { keyPath: 'id' })
}
2024-03-18 11:07:28 +00:00
}
2024-06-28 11:45:25 +00:00
})
console.log('Database opened successfully:', db)
return db
} catch (e) {
console.error('Failed to open IndexedDB:', e)
}
2024-03-18 11:07:28 +00:00
}
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
}
}
2024-07-05 19:40:54 +00:00
export type TopicSort = 'shouts' | 'followers' | 'authors' | 'title'
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 }>({})
2024-06-28 15:05:45 +00:00
const [sortedTopics, setSortedTopics] = createSignal<Topic[]>([])
2024-07-05 19:40:54 +00:00
const [sortAllBy, setSortAllBy] = createSignal<TopicSort>('shouts')
2024-03-18 11:07:28 +00:00
2024-06-28 15:05:45 +00:00
createEffect(() => {
2024-05-06 23:44:25 +00:00
const topics = Object.values(topicEntities())
2024-07-05 22:24:22 +00:00
// console.debug('[context.topics] effect trig', topics)
2024-05-06 23:44:25 +00:00
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'))
}
}
2024-06-28 15:05:45 +00:00
setSortedTopics(topics as Topic[])
2024-05-06 23:44:25 +00:00
})
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
},
2024-06-26 08:22:05 +00:00
{} as Record<string, Topic>
2024-05-06 23:44:25 +00:00
)
setTopicEntities((prevTopicEntities) => {
2024-08-05 11:03:30 +00:00
const ttt = {
2024-05-06 23:44:25 +00:00
...prevTopicEntities,
2024-06-26 08:22:05 +00:00
...newTopicEntities
2024-05-06 23:44:25 +00:00
}
2024-08-05 11:03:30 +00:00
if (db()) saveTopicsToIndexedDB(db() as IDBDatabase, Object.values(ttt) as Topic[])
return ttt
2024-05-06 23:44:25 +00:00
})
}
2024-05-07 00:05:04 +00:00
const [db, setDb] = createSignal()
2024-07-18 11:22:59 +00:00
createEffect(
on(
() => window?.indexedDB,
async (_raw) => {
const initialized = await setupIndexedDB()
setDb(initialized)
},
{ defer: true }
)
)
2024-06-24 17:50:27 +00:00
const loadAllTopics = async () => {
const topicsLoader = loadTopics()
const ttt = await topicsLoader()
2024-07-18 11:22:59 +00:00
ttt && addTopics(ttt)
2024-06-24 17:50:27 +00:00
return ttt || []
2024-05-07 00:05:04 +00:00
}
2024-05-06 23:44:25 +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()
2024-06-28 15:05:45 +00:00
console.info(`[context.topics] got ${(topics as Topic[]).length || 0} topics from idb`)
2024-06-24 17:50:27 +00:00
addTopics(topics as Topic[])
2024-08-30 13:45:17 +00:00
setRandomTopic(getRandomItemsFromArray(topics || [], 1).pop())
2024-06-24 17:50:27 +00:00
}
},
2024-06-26 08:22:05 +00:00
{ defer: true }
)
2024-06-24 17:50:27 +00:00
)
2024-07-18 11:22:59 +00:00
const getCachedOrLoadTopics = async () => {
const { topics: stored } = await getTopicsFromIndexedDB(db() as IDBDatabase)
if (stored) {
setSortedTopics(stored)
return stored
}
const loaded = await loadAllTopics()
if (loaded) setSortedTopics(loaded)
return loaded
}
// preload all topics
onMount(getCachedOrLoadTopics)
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-26 08:22:05 +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>
}