import { Conf, d, g } from '../globals/globals' import $ from '../platform/$' import { dict, HOUR } from '../platform/helpers' export default class DataBoard { static initClass() { this.keys = [ 'hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'watcherLastModified', 'customTitles', ] this.changes = [] } constructor(key, sync, dontClean) { this.onSync = this.onSync.bind(this) this.key = key this.initData(Conf[this.key]) $.sync(this.key, this.onSync) if (!dontClean) { this.clean() } if (!sync) { return } // Chrome also fires the onChanged callback on the current tab, // so we only start syncing when we're ready. var init = () => { $.off(d, '4chanXInitFinished', init) return (this.sync = sync) } $.on(d, '4chanXInitFinished', init) } initData(data) { let boards this.data = data if (this.data.boards) { let lastChecked ;({ boards, lastChecked } = this.data) this.data['4chan.org'] = { boards, lastChecked } delete this.data.boards delete this.data.lastChecked } return this.data[g.SITE.ID] || (this.data[g.SITE.ID] = { boards: dict() }) } save(change, cb) { change() DataBoard.changes.push(change) return $.get(this.key, { boards: dict() }, items => { if (!DataBoard.changes.length) { return } const needSync = (items[this.key].version || 0) > (this.data.version || 0) if (needSync) { this.initData(items[this.key]) for (change of DataBoard.changes) { change() } } DataBoard.changes = [] this.data.version = (this.data.version || 0) + 1 return $.set(this.key, this.data, () => { if (needSync) { this.sync?.() } return cb?.() }) }) } forceSync(cb) { return $.get(this.key, { boards: dict() }, items => { if ((items[this.key].version || 0) > (this.data.version || 0)) { this.initData(items[this.key]) for (var change of DataBoard.changes) { change() } this.sync?.() } return cb?.() }) } delete({ siteID, boardID, threadID, postID }, cb) { if (!siteID) { siteID = g.SITE.ID } if (!this.data[siteID]) { return } return this.save(() => { if (postID) { if (!this.data[siteID].boards[boardID]?.[threadID]) { return } delete this.data[siteID].boards[boardID][threadID][postID] return this.deleteIfEmpty({ siteID, boardID, threadID }) } else if (threadID) { if (!this.data[siteID].boards[boardID]) { return } delete this.data[siteID].boards[boardID][threadID] return this.deleteIfEmpty({ siteID, boardID, threadID }) } else { return delete this.data[siteID].boards[boardID] } }, cb) } deleteIfEmpty({ siteID, boardID, threadID }) { if (!this.data[siteID]) { return } if (threadID) { if (!Object.keys(this.data[siteID].boards[boardID][threadID]).length) { delete this.data[siteID].boards[boardID][threadID] return this.deleteIfEmpty({ siteID, boardID, threadID: null }) } } else if (!Object.keys(this.data[siteID].boards[boardID]).length) { return delete this.data[siteID].boards[boardID] } } set(data, cb) { return this.save(() => { return this.setUnsafe(data) }, cb) } setUnsafe({ siteID, boardID, threadID, postID, val }) { if (!siteID) { siteID = g.SITE.ID } if (!this.data[siteID]) { this.data[siteID] = { boards: dict() } } if (postID !== undefined) { let base return (((base = this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict()))[threadID] || (base[threadID] = dict()))[postID] = val) } else if (threadID !== undefined) { return ((this.data[siteID].boards[boardID] || (this.data[siteID].boards[boardID] = dict()))[threadID] = val) } else { return (this.data[siteID].boards[boardID] = val) } } extend({ siteID, boardID, threadID, postID, val }, cb) { return this.save(() => { const oldVal = this.get({ siteID, boardID, threadID, postID, defaultValue: dict(), }) for (var key in val) { var subVal = val[key] if (typeof subVal === 'undefined') { delete oldVal[key] } else { oldVal[key] = subVal } } return this.setUnsafe({ siteID, boardID, threadID, postID, val: oldVal }) }, cb) } setLastChecked(key = 'lastChecked') { return this.save(() => { return (this.data[key] = Date.now()) }) } get({ siteID, boardID, threadID, postID, defaultValue }) { let board, val if (!siteID) { siteID = g.SITE.ID } if ((board = this.data[siteID]?.boards[boardID])) { let thread if (threadID == null) { if (postID != null) { for (thread = 0; thread < board.length; thread++) { var ID = board[thread] if (ID == postID) { val = thread[postID] break } } } else { val = board } } else if ((thread = board[threadID])) { val = postID != null ? thread[postID] : thread } } return val || defaultValue } clean() { let boardID, middle const siteID = g.SITE.ID for (boardID in this.data[siteID].boards) { var val = this.data[siteID].boards[boardID] this.deleteIfEmpty({ siteID, boardID, threadID: val }) } const now = Date.now() if ( now - 2 * HOUR >= (middle = this.data[siteID].lastChecked || 0) || middle > now ) { this.data[siteID].lastChecked = now for (boardID in this.data[siteID].boards) { this.ajaxClean(boardID) } } } ajaxClean(boardID) { const that = this const siteID = g.SITE.ID const threadsList = g.SITE.urls.threadsListJSON?.({ siteID, boardID }) if (!threadsList) { return } return $.cache(threadsList, function () { if (this.status !== 200) { return } const archiveList = g.SITE.urls.archiveListJSON?.({ siteID, boardID }) if (!archiveList) { return that.ajaxCleanParse(boardID, this.response) } const response1 = this.response return $.cache(archiveList, function () { if ( this.status !== 200 && (!!g.SITE.archivedBoardsKnown || this.status !== 404) ) { return } return that.ajaxCleanParse(boardID, response1, this.response) }) }) } ajaxCleanParse(boardID, response1, response2) { let board, ID const siteID = g.SITE.ID if (!(board = this.data[siteID].boards[boardID])) { return } const threads = dict() if (response1) { for (var page of response1) { for (var thread of page.threads) { ID = thread.no if (ID in board) { threads[ID] = board[ID] } } } } if (response2) { for (ID of response2) { if (ID in board) { threads[ID] = board[ID] } } } this.data[siteID].boards[boardID] = threads this.deleteIfEmpty({ siteID, boardID, threadID: null }) return $.set(this.key, this.data) } onSync(data) { if ((data.version || 0) <= (this.data.version || 0)) { return } this.initData(data) return this.sync?.() } } DataBoard.initClass()