refactor(platform): Phase 752 forum search & view consistency mixins (v0.8.728)
Extract forum post local/mesh search, partition forum index heal, and channel view pin/reaction consistency audit into three prototype mixins (manifest 101 rows, runtime registry 41). Non-breaking: identical forum search, partition heal, and view consistency audit behavior; platform class ~31564 lines (~311 net drop). Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -54,6 +54,8 @@ Application facade: one `PearcordPlatform` class that wires identity, database,
|
||||
|
||||
**Phase 723 (v0.8.699):** Dedicated slash registry & invoke audit export JSON — `slash-registry-json-mixin.js`, `slash-invoke-audit-json-mixin.js`, spans `slash-registry.json` / `slash-invoke-audit.json`, `getSlashInvokeAuditExportJsonExport`, `clearSlashInvokeAuditExportJsonExports`, deep links `openSlashRegistryJson` / `openSlashInvokeAuditJson`. Bundle: `npm run test:ci-phase723`.
|
||||
|
||||
**Phase 752 (v0.8.728):** `platform-forum-search-mixin.js`, `platform-forum-partition-heal-mixin.js`, `platform-view-channel-consistency-mixin.js` (101 mixin rows, 41 runtime mixins); forum post local/mesh search, partition forum index heal, channel view consistency audit. Bundle: `npm run test:ci-phase752`.
|
||||
|
||||
**Phase 751 (v0.8.727):** `platform-search-guild-messages-mixin.js`, `platform-search-query-filters-mixin.js`, `platform-search-mesh-resolve-mixin.js` (98 mixin rows, 38 runtime mixins); guild message search, structured query filters, index meta, mesh hit merge. Bundle: `npm run test:ci-phase751`.
|
||||
|
||||
**Phase 750 (v0.8.726):** `platform-search-index-store-mixin.js`, `platform-search-index-rebuild-mixin.js`, `platform-search-mesh-gossip-mixin.js`, `platform-discovery-mesh-refresh-mixin.js` (95 mixin rows, 35 runtime mixins); guild search index store/persist/rebuild, mesh search gossip, discovery listing refresh after mesh. Bundle: `npm run test:ci-phase750`.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict'
|
||||
|
||||
const platformForumPartitionHealMixin = {
|
||||
async _healForumSearchIndexOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id
|
||||
if (!gid) return { forumRows: 0, searchUpdated: false }
|
||||
const channels = (await this.guild?.listChannels?.().catch(() => [])) || []
|
||||
const forumRows = await this._exportForumIndexForSyncBundle(gid, channels, 0)
|
||||
const forumApplied = await this._ingestForumIndexBundle(gid, forumRows)
|
||||
let searchUpdated = false
|
||||
if (typeof this.rebuildGuildSearchIndex === 'function') {
|
||||
await this.rebuildGuildSearchIndex(gid).catch(() => null)
|
||||
searchUpdated = true
|
||||
}
|
||||
return { forumRows: forumRows.length, forumApplied, searchUpdated }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { platformForumPartitionHealMixin }
|
||||
@@ -0,0 +1,134 @@
|
||||
'use strict'
|
||||
|
||||
const sharedScope = require('pearcord-shared')
|
||||
const threadsScope = require('pearcord-threads')
|
||||
const searchScope = require('pearcord-search')
|
||||
const enrichmentScope = require('./platform-message-enrichment')
|
||||
const { forumPostHaystack } = require('./platform-class-imports')
|
||||
|
||||
const platformForumSearchMixin = {
|
||||
async searchForumPosts (guildId, forumChannelId, query, opts = {}) {
|
||||
const q = String(query || '').trim().toLowerCase()
|
||||
if (!q || !guildId || !forumChannelId) return []
|
||||
const limit = opts.limit || 50
|
||||
const includeArchived = opts.includeArchived !== false
|
||||
const channels = await this.guild?.listChannels?.() || await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId })
|
||||
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, { includeArchived })
|
||||
const forumCh = channels.find((c) => c.id === forumChannelId)
|
||||
const hits = []
|
||||
for (const post of posts) {
|
||||
const tags = this.forum ? await this.forum.getPostTags(guildId, post.id) : []
|
||||
let content = ''
|
||||
if (post.rootMessageId) {
|
||||
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId: post.id,
|
||||
id: post.rootMessageId
|
||||
})
|
||||
content = root ? this._messagePlaintext(enrichmentScope.enrichMessageRow(root)) : ''
|
||||
} else {
|
||||
const rows = await this.db.find(sharedScope.COLLECTIONS.MESSAGES, { channelId: post.id })
|
||||
if (rows[0]) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(rows[0]))
|
||||
}
|
||||
const haystack = forumPostHaystack({
|
||||
title: post.name,
|
||||
content,
|
||||
tags,
|
||||
archived: post.archived
|
||||
})
|
||||
if (!haystack.includes(q)) continue
|
||||
hits.push({
|
||||
...post,
|
||||
tags,
|
||||
forumChannelId,
|
||||
forumChannelName: forumCh?.name || 'forum',
|
||||
searchHaystack: haystack,
|
||||
snippet: String(content || '').slice(0, 160)
|
||||
})
|
||||
}
|
||||
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit })
|
||||
},
|
||||
|
||||
async _forumPostsFromMeshMessages (guildId, forumChannelId, messages, query) {
|
||||
const q = String(query || '').trim()
|
||||
if (!q || !guildId || !forumChannelId) return []
|
||||
const channels =
|
||||
(await this.guild?.listChannels?.()) ||
|
||||
(await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId }))
|
||||
const forumCh = channels.find((c) => c.id === forumChannelId)
|
||||
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, {
|
||||
includeArchived: true
|
||||
})
|
||||
const byThread = new Map(posts.map((p) => [p.id, p]))
|
||||
const hits = []
|
||||
for (const m of messages || []) {
|
||||
const post = byThread.get(m.channelId)
|
||||
if (!post) continue
|
||||
let content = String(m.content || '')
|
||||
if (!content && post.rootMessageId) {
|
||||
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId: post.id,
|
||||
id: post.rootMessageId
|
||||
})
|
||||
if (root) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(root))
|
||||
}
|
||||
const tags = this.forum
|
||||
? await this.forum.getPostTags(guildId, post.id).catch(() => [])
|
||||
: []
|
||||
hits.push({
|
||||
...post,
|
||||
tags,
|
||||
forumChannelId,
|
||||
forumChannelName: forumCh?.name || 'forum',
|
||||
searchHaystack: forumPostHaystack({
|
||||
title: post.name,
|
||||
content,
|
||||
tags,
|
||||
archived: post.archived
|
||||
}),
|
||||
snippet: String(content || '').slice(0, 160),
|
||||
fromMesh: true
|
||||
})
|
||||
}
|
||||
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit: 50 })
|
||||
},
|
||||
|
||||
async searchForumPostsWithMesh (guildId, forumChannelId, query, opts = {}) {
|
||||
const local = await this.searchForumPosts(guildId, forumChannelId, query, opts)
|
||||
if (opts.mesh === false || !this.guild?.guild) {
|
||||
return {
|
||||
hits: local,
|
||||
mesh: false,
|
||||
meshHitCount: 0,
|
||||
localHitCount: local.length
|
||||
}
|
||||
}
|
||||
try {
|
||||
const meshMsgs = await this._fetchGuildSearchFromMeshOnce(guildId, query, opts)
|
||||
const meshPosts = await this._forumPostsFromMeshMessages(
|
||||
guildId,
|
||||
forumChannelId,
|
||||
meshMsgs.map((m) => enrichmentScope.enrichMessageRow(m)),
|
||||
query
|
||||
)
|
||||
const merged = searchScope.mergeForumSearchHits(local, meshPosts, {
|
||||
limit: opts.limit || 50,
|
||||
query: String(query || '').trim()
|
||||
})
|
||||
return {
|
||||
hits: merged,
|
||||
mesh: true,
|
||||
meshHitCount: meshPosts.length,
|
||||
localHitCount: local.length
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
hits: local,
|
||||
mesh: false,
|
||||
meshHitCount: 0,
|
||||
localHitCount: local.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { platformForumSearchMixin }
|
||||
@@ -4,7 +4,7 @@
|
||||
* Ordered PearcordPlatform prototype mixin registration (Phase 730).
|
||||
* Preserves assign order from index.js / apply-platform-mixins (Phase 727).
|
||||
*/
|
||||
const PLATFORM_MIXIN_ASSIGNMENT_COUNT = 98
|
||||
const PLATFORM_MIXIN_ASSIGNMENT_COUNT = 101
|
||||
|
||||
const PLATFORM_MIXIN_ASSIGNMENTS = [
|
||||
{ module: './polls-scheduling', export: 'pollSchedulingMixin' },
|
||||
@@ -104,6 +104,9 @@ const PLATFORM_MIXIN_ASSIGNMENTS = [
|
||||
{ module: './platform-search-guild-messages-mixin', export: 'platformSearchGuildMessagesMixin' },
|
||||
{ module: './platform-search-query-filters-mixin', export: 'platformSearchQueryFiltersMixin' },
|
||||
{ module: './platform-search-mesh-resolve-mixin', export: 'platformSearchMeshResolveMixin' },
|
||||
{ module: './platform-forum-search-mixin', export: 'platformForumSearchMixin' },
|
||||
{ module: './platform-forum-partition-heal-mixin', export: 'platformForumPartitionHealMixin' },
|
||||
{ module: './platform-view-channel-consistency-mixin', export: 'platformViewChannelConsistencyMixin' },
|
||||
{ module: './platform-diagnostics-mixin', export: 'platformDiagnosticsMixin' }
|
||||
]
|
||||
|
||||
|
||||
@@ -420,19 +420,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
return { links, roleCount: roles.length }
|
||||
}
|
||||
|
||||
async _healForumSearchIndexOnPartition (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id
|
||||
if (!gid) return { forumRows: 0, searchUpdated: false }
|
||||
const channels = (await this.guild?.listChannels?.().catch(() => [])) || []
|
||||
const forumRows = await this._exportForumIndexForSyncBundle(gid, channels, 0)
|
||||
const forumApplied = await this._ingestForumIndexBundle(gid, forumRows)
|
||||
let searchUpdated = false
|
||||
if (typeof this.rebuildGuildSearchIndex === 'function') {
|
||||
await this.rebuildGuildSearchIndex(gid).catch(() => null)
|
||||
searchUpdated = true
|
||||
}
|
||||
return { forumRows: forumRows.length, forumApplied, searchUpdated }
|
||||
}
|
||||
|
||||
|
||||
async _healSettingsMeshDeviceSync (guildId) {
|
||||
const gid = guildId || this.guild?.guild?.id
|
||||
@@ -964,188 +952,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
* Compare messages, reactions, and pins for the active channel during view build.
|
||||
* Invalidates pin/reaction list caches when view-layer data disagrees with DB rows.
|
||||
*/
|
||||
async _auditChannelViewConsistency (opts = {}) {
|
||||
const channelId = opts.channelId || this.activeChannelId
|
||||
const guildId =
|
||||
opts.guildId ??
|
||||
(opts.mode === 'dm' ? dmScope.DM_GUILD_ID : this.guild?.guild?.id ?? null)
|
||||
const skipped = { ok: true, skipped: true, channelId: null, issues: [], counts: {} }
|
||||
if (!channelId) return skipped
|
||||
|
||||
const messages = opts.messages || []
|
||||
const messageById = opts.messageById || {}
|
||||
const reactions = opts.reactions || {}
|
||||
const pins = opts.pins || []
|
||||
const light = opts.light === true
|
||||
const detail = opts.detail || 'full'
|
||||
const issues = []
|
||||
const counts = {
|
||||
messageCount: messages.length,
|
||||
reactionMessageIds: 0,
|
||||
pinCount: pins.length,
|
||||
reactionsOutsideHistory: 0,
|
||||
pinsOutsideHistory: 0,
|
||||
orphanReactionRows: 0,
|
||||
orphanPinRows: 0,
|
||||
reactionCacheMismatches: 0,
|
||||
pinCacheMismatches: 0,
|
||||
repaired: 0
|
||||
}
|
||||
const historyIds = new Set(messages.map((m) => m.id))
|
||||
|
||||
for (const messageId of Object.keys(reactions)) {
|
||||
counts.reactionMessageIds++
|
||||
if (!messageById[messageId]) {
|
||||
counts.reactionsOutsideHistory++
|
||||
if (detail === 'diagnostic') {
|
||||
issues.push({
|
||||
kind: 'reaction-outside-history',
|
||||
messageId,
|
||||
severity: 'info'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pin of pins) {
|
||||
const mid = pin.messageId
|
||||
if (!mid) continue
|
||||
if (!pin.message && !messageById[mid]) {
|
||||
issues.push({
|
||||
kind: 'pin-missing-embedded-message',
|
||||
messageId: mid,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
if (!historyIds.has(mid)) counts.pinsOutsideHistory++
|
||||
}
|
||||
|
||||
let repaired = false
|
||||
if (!light && this.messages?.channelId === channelId) {
|
||||
const reactionRows = await this.messages.listReactions().catch(() => [])
|
||||
const reactionMsgIds = new Set(reactionRows.map((r) => r.messageId))
|
||||
|
||||
for (const messageId of Object.keys(reactions)) {
|
||||
if (!reactionMsgIds.has(messageId)) {
|
||||
counts.reactionCacheMismatches++
|
||||
issues.push({
|
||||
kind: 'reaction-phantom-in-view',
|
||||
messageId,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const mid of reactionMsgIds) {
|
||||
if (!reactions[mid]) {
|
||||
counts.reactionCacheMismatches++
|
||||
issues.push({
|
||||
kind: 'reaction-missing-in-view',
|
||||
messageId: mid,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of reactionRows) {
|
||||
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId,
|
||||
id: r.messageId
|
||||
})
|
||||
if (!msg) {
|
||||
counts.orphanReactionRows++
|
||||
issues.push({
|
||||
kind: 'orphan-reaction-row',
|
||||
messageId: r.messageId,
|
||||
severity: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pinRows = await this.db.find(sharedScope.COLLECTIONS.PINS, { channelId })
|
||||
counts.pinRowCount = pinRows.length
|
||||
const pinViewIds = new Set(pins.map((p) => p.messageId).filter(Boolean))
|
||||
|
||||
for (const pin of pinRows) {
|
||||
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId: pin.channelId || channelId,
|
||||
id: pin.messageId
|
||||
})
|
||||
if (!msg) {
|
||||
counts.orphanPinRows++
|
||||
issues.push({
|
||||
kind: 'orphan-pin-row',
|
||||
messageId: pin.messageId,
|
||||
severity: 'error'
|
||||
})
|
||||
const removed = await this._repairOrphanPinRow(pin, {
|
||||
channelId,
|
||||
guildId,
|
||||
source: 'view-consistency-audit'
|
||||
})
|
||||
if (removed) {
|
||||
counts.repaired += 1
|
||||
repaired = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!pinViewIds.has(pin.messageId)) {
|
||||
counts.pinCacheMismatches++
|
||||
issues.push({
|
||||
kind: 'pin-missing-in-view',
|
||||
messageId: pin.messageId,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (counts.reactionCacheMismatches > 0) {
|
||||
this._invalidateReactionListCache()
|
||||
repaired = true
|
||||
counts.repaired += 1
|
||||
}
|
||||
if (counts.pinCacheMismatches > 0) {
|
||||
this._invalidatePinListCache()
|
||||
repaired = true
|
||||
counts.repaired += 1
|
||||
}
|
||||
|
||||
const bad = issues.filter((i) => i.severity === 'warn' || i.severity === 'error')
|
||||
const ok = bad.length === 0 && !counts.orphanReactionRows && !counts.orphanPinRows
|
||||
const span = this.log.time('view.channel-consistency', {
|
||||
spanKind: 'view.channel-consistency',
|
||||
channelId,
|
||||
guildId
|
||||
})
|
||||
span.end({
|
||||
ok,
|
||||
repaired,
|
||||
...counts,
|
||||
issueCount: issues.length,
|
||||
logLevel: ok ? 'debug' : 'info'
|
||||
})
|
||||
if (!ok) {
|
||||
this.log.info('view.channel-consistency issues', {
|
||||
spanKind: 'view.channel-consistency',
|
||||
channelId,
|
||||
guildId,
|
||||
ok,
|
||||
repaired,
|
||||
counts,
|
||||
issues: issues.slice(0, 8)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ok,
|
||||
channelId,
|
||||
guildId,
|
||||
counts,
|
||||
issues: issues.slice(0, 32),
|
||||
repaired
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a pin row whose message body no longer exists (P410-46).
|
||||
@@ -4597,128 +4404,11 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.emit('forum-filter')
|
||||
}
|
||||
|
||||
async searchForumPosts (guildId, forumChannelId, query, opts = {}) {
|
||||
const q = String(query || '').trim().toLowerCase()
|
||||
if (!q || !guildId || !forumChannelId) return []
|
||||
const limit = opts.limit || 50
|
||||
const includeArchived = opts.includeArchived !== false
|
||||
const channels = await this.guild?.listChannels?.() || await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId })
|
||||
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, { includeArchived })
|
||||
const forumCh = channels.find((c) => c.id === forumChannelId)
|
||||
const hits = []
|
||||
for (const post of posts) {
|
||||
const tags = this.forum ? await this.forum.getPostTags(guildId, post.id) : []
|
||||
let content = ''
|
||||
if (post.rootMessageId) {
|
||||
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId: post.id,
|
||||
id: post.rootMessageId
|
||||
})
|
||||
content = root ? this._messagePlaintext(enrichmentScope.enrichMessageRow(root)) : ''
|
||||
} else {
|
||||
const rows = await this.db.find(sharedScope.COLLECTIONS.MESSAGES, { channelId: post.id })
|
||||
if (rows[0]) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(rows[0]))
|
||||
}
|
||||
const haystack = forumPostHaystack({
|
||||
title: post.name,
|
||||
content,
|
||||
tags,
|
||||
archived: post.archived
|
||||
})
|
||||
if (!haystack.includes(q)) continue
|
||||
hits.push({
|
||||
...post,
|
||||
tags,
|
||||
forumChannelId,
|
||||
forumChannelName: forumCh?.name || 'forum',
|
||||
searchHaystack: haystack,
|
||||
snippet: String(content || '').slice(0, 160)
|
||||
})
|
||||
}
|
||||
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit })
|
||||
}
|
||||
|
||||
async _forumPostsFromMeshMessages (guildId, forumChannelId, messages, query) {
|
||||
const q = String(query || '').trim()
|
||||
if (!q || !guildId || !forumChannelId) return []
|
||||
const channels =
|
||||
(await this.guild?.listChannels?.()) ||
|
||||
(await this.db.find(sharedScope.COLLECTIONS.CHANNELS, { guildId }))
|
||||
const forumCh = channels.find((c) => c.id === forumChannelId)
|
||||
const posts = threadsScope.listForumPostsForParent(channels, forumChannelId, {
|
||||
includeArchived: true
|
||||
})
|
||||
const byThread = new Map(posts.map((p) => [p.id, p]))
|
||||
const hits = []
|
||||
for (const m of messages || []) {
|
||||
const post = byThread.get(m.channelId)
|
||||
if (!post) continue
|
||||
let content = String(m.content || '')
|
||||
if (!content && post.rootMessageId) {
|
||||
const root = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId: post.id,
|
||||
id: post.rootMessageId
|
||||
})
|
||||
if (root) content = this._messagePlaintext(enrichmentScope.enrichMessageRow(root))
|
||||
}
|
||||
const tags = this.forum
|
||||
? await this.forum.getPostTags(guildId, post.id).catch(() => [])
|
||||
: []
|
||||
hits.push({
|
||||
...post,
|
||||
tags,
|
||||
forumChannelId,
|
||||
forumChannelName: forumCh?.name || 'forum',
|
||||
searchHaystack: forumPostHaystack({
|
||||
title: post.name,
|
||||
content,
|
||||
tags,
|
||||
archived: post.archived
|
||||
}),
|
||||
snippet: String(content || '').slice(0, 160),
|
||||
fromMesh: true
|
||||
})
|
||||
}
|
||||
return searchScope.rankAndEnrichForumSearchHits(hits, q, { limit: 50 })
|
||||
}
|
||||
|
||||
async searchForumPostsWithMesh (guildId, forumChannelId, query, opts = {}) {
|
||||
const local = await this.searchForumPosts(guildId, forumChannelId, query, opts)
|
||||
if (opts.mesh === false || !this.guild?.guild) {
|
||||
return {
|
||||
hits: local,
|
||||
mesh: false,
|
||||
meshHitCount: 0,
|
||||
localHitCount: local.length
|
||||
}
|
||||
}
|
||||
try {
|
||||
const meshMsgs = await this._fetchGuildSearchFromMeshOnce(guildId, query, opts)
|
||||
const meshPosts = await this._forumPostsFromMeshMessages(
|
||||
guildId,
|
||||
forumChannelId,
|
||||
meshMsgs.map((m) => enrichmentScope.enrichMessageRow(m)),
|
||||
query
|
||||
)
|
||||
const merged = searchScope.mergeForumSearchHits(local, meshPosts, {
|
||||
limit: opts.limit || 50,
|
||||
query: String(query || '').trim()
|
||||
})
|
||||
return {
|
||||
hits: merged,
|
||||
mesh: true,
|
||||
meshHitCount: meshPosts.length,
|
||||
localHitCount: local.length
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
hits: local,
|
||||
mesh: false,
|
||||
meshHitCount: 0,
|
||||
localHitCount: local.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738–751). */
|
||||
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738–752). */
|
||||
const PLATFORM_RUNTIME_MIXIN_MODULES = [
|
||||
'./platform-hyperswarm-runtime-mixin',
|
||||
'./platform-session-startup-mixin',
|
||||
@@ -39,7 +39,10 @@ const PLATFORM_RUNTIME_MIXIN_MODULES = [
|
||||
'./platform-discovery-mesh-refresh-mixin',
|
||||
'./platform-search-guild-messages-mixin',
|
||||
'./platform-search-query-filters-mixin',
|
||||
'./platform-search-mesh-resolve-mixin'
|
||||
'./platform-search-mesh-resolve-mixin',
|
||||
'./platform-forum-search-mixin',
|
||||
'./platform-forum-partition-heal-mixin',
|
||||
'./platform-view-channel-consistency-mixin'
|
||||
]
|
||||
|
||||
const PLATFORM_RUNTIME_MIXIN_COUNT = PLATFORM_RUNTIME_MIXIN_MODULES.length
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
'use strict'
|
||||
|
||||
const dmScope = require('pearcord-dm')
|
||||
const sharedScope = require('pearcord-shared')
|
||||
|
||||
const platformViewChannelConsistencyMixin = {
|
||||
async _auditChannelViewConsistency (opts = {}) {
|
||||
const channelId = opts.channelId || this.activeChannelId
|
||||
const guildId =
|
||||
opts.guildId ??
|
||||
(opts.mode === 'dm' ? dmScope.DM_GUILD_ID : this.guild?.guild?.id ?? null)
|
||||
const skipped = { ok: true, skipped: true, channelId: null, issues: [], counts: {} }
|
||||
if (!channelId) return skipped
|
||||
|
||||
const messages = opts.messages || []
|
||||
const messageById = opts.messageById || {}
|
||||
const reactions = opts.reactions || {}
|
||||
const pins = opts.pins || []
|
||||
const light = opts.light === true
|
||||
const detail = opts.detail || 'full'
|
||||
const issues = []
|
||||
const counts = {
|
||||
messageCount: messages.length,
|
||||
reactionMessageIds: 0,
|
||||
pinCount: pins.length,
|
||||
reactionsOutsideHistory: 0,
|
||||
pinsOutsideHistory: 0,
|
||||
orphanReactionRows: 0,
|
||||
orphanPinRows: 0,
|
||||
reactionCacheMismatches: 0,
|
||||
pinCacheMismatches: 0,
|
||||
repaired: 0
|
||||
}
|
||||
const historyIds = new Set(messages.map((m) => m.id))
|
||||
|
||||
for (const messageId of Object.keys(reactions)) {
|
||||
counts.reactionMessageIds++
|
||||
if (!messageById[messageId]) {
|
||||
counts.reactionsOutsideHistory++
|
||||
if (detail === 'diagnostic') {
|
||||
issues.push({
|
||||
kind: 'reaction-outside-history',
|
||||
messageId,
|
||||
severity: 'info'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pin of pins) {
|
||||
const mid = pin.messageId
|
||||
if (!mid) continue
|
||||
if (!pin.message && !messageById[mid]) {
|
||||
issues.push({
|
||||
kind: 'pin-missing-embedded-message',
|
||||
messageId: mid,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
if (!historyIds.has(mid)) counts.pinsOutsideHistory++
|
||||
}
|
||||
|
||||
let repaired = false
|
||||
if (!light && this.messages?.channelId === channelId) {
|
||||
const reactionRows = await this.messages.listReactions().catch(() => [])
|
||||
const reactionMsgIds = new Set(reactionRows.map((r) => r.messageId))
|
||||
|
||||
for (const messageId of Object.keys(reactions)) {
|
||||
if (!reactionMsgIds.has(messageId)) {
|
||||
counts.reactionCacheMismatches++
|
||||
issues.push({
|
||||
kind: 'reaction-phantom-in-view',
|
||||
messageId,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const mid of reactionMsgIds) {
|
||||
if (!reactions[mid]) {
|
||||
counts.reactionCacheMismatches++
|
||||
issues.push({
|
||||
kind: 'reaction-missing-in-view',
|
||||
messageId: mid,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of reactionRows) {
|
||||
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId,
|
||||
id: r.messageId
|
||||
})
|
||||
if (!msg) {
|
||||
counts.orphanReactionRows++
|
||||
issues.push({
|
||||
kind: 'orphan-reaction-row',
|
||||
messageId: r.messageId,
|
||||
severity: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const pinRows = await this.db.find(sharedScope.COLLECTIONS.PINS, { channelId })
|
||||
counts.pinRowCount = pinRows.length
|
||||
const pinViewIds = new Set(pins.map((p) => p.messageId).filter(Boolean))
|
||||
|
||||
for (const pin of pinRows) {
|
||||
const msg = await this.db.get(sharedScope.COLLECTIONS.MESSAGES, {
|
||||
channelId: pin.channelId || channelId,
|
||||
id: pin.messageId
|
||||
})
|
||||
if (!msg) {
|
||||
counts.orphanPinRows++
|
||||
issues.push({
|
||||
kind: 'orphan-pin-row',
|
||||
messageId: pin.messageId,
|
||||
severity: 'error'
|
||||
})
|
||||
const removed = await this._repairOrphanPinRow(pin, {
|
||||
channelId,
|
||||
guildId,
|
||||
source: 'view-consistency-audit'
|
||||
})
|
||||
if (removed) {
|
||||
counts.repaired += 1
|
||||
repaired = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!pinViewIds.has(pin.messageId)) {
|
||||
counts.pinCacheMismatches++
|
||||
issues.push({
|
||||
kind: 'pin-missing-in-view',
|
||||
messageId: pin.messageId,
|
||||
severity: 'warn'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (counts.reactionCacheMismatches > 0) {
|
||||
this._invalidateReactionListCache()
|
||||
repaired = true
|
||||
counts.repaired += 1
|
||||
}
|
||||
if (counts.pinCacheMismatches > 0) {
|
||||
this._invalidatePinListCache()
|
||||
repaired = true
|
||||
counts.repaired += 1
|
||||
}
|
||||
|
||||
const bad = issues.filter((i) => i.severity === 'warn' || i.severity === 'error')
|
||||
const ok = bad.length === 0 && !counts.orphanReactionRows && !counts.orphanPinRows
|
||||
const span = this.log.time('view.channel-consistency', {
|
||||
spanKind: 'view.channel-consistency',
|
||||
channelId,
|
||||
guildId
|
||||
})
|
||||
span.end({
|
||||
ok,
|
||||
repaired,
|
||||
...counts,
|
||||
issueCount: issues.length,
|
||||
logLevel: ok ? 'debug' : 'info'
|
||||
})
|
||||
if (!ok) {
|
||||
this.log.info('view.channel-consistency issues', {
|
||||
spanKind: 'view.channel-consistency',
|
||||
channelId,
|
||||
guildId,
|
||||
ok,
|
||||
repaired,
|
||||
counts,
|
||||
issues: issues.slice(0, 8)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
ok,
|
||||
channelId,
|
||||
guildId,
|
||||
counts,
|
||||
issues: issues.slice(0, 32),
|
||||
repaired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { platformViewChannelConsistencyMixin }
|
||||
Reference in New Issue
Block a user