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'
|
2024-07-04 07:51:15 +00:00
|
|
|
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>
|
|
|
|
}
|