Files
pearcord-platform/guild-sync-replay.js
T
Raven ScottandCursor fc344fac87 Phase 408: GUILD_SYNC replay and mark-read fanout hardening
Add guild-sync-replay tracker with syncBundleId on export bundles to skip
duplicate message applies after partial ingest. Coalesce rapid channel/thread
mark-read through MarkReadFanoutQueue with stall watchdog. Reject oversized
automation hook payloads via sanitizeAutomationEventPayload.

Co-authored-by: Cursor <[email protected]>
2026-06-02 16:24:13 -04:00

111 lines
3.1 KiB
JavaScript

'use strict'
/** Ordered ingest phases for partial GUILD_SYNC resume (P408-15). */
const GUILD_SYNC_INGEST_PHASES = [
'meta',
'members',
'channels',
'messages',
'reactions',
'sparse'
]
function deriveSyncBundleId (payload) {
if (!payload?.guildId) return null
if (payload.syncBundleId) return String(payload.syncBundleId)
const seq = payload.syncSeq ?? payload.syncGeneration ?? 0
const tail =
payload.sinceMessageId ||
String(payload.sinceTimestamp || 0) ||
(payload.messages?.length
? `${payload.messages[payload.messages.length - 1]?.channelId}:${payload.messages[payload.messages.length - 1]?.id}`
: 'full')
return `${payload.guildId}:${seq}:${tail}`
}
class GuildSyncReplayTracker {
constructor () {
/** @type {Map<string, { bundleId: string, partial: boolean, completedPhases: string[], startedAt: number, finishedAt?: number }>} */
this.byGuild = new Map()
/** @type {Map<string, Set<string>>} */
this.appliedMessageKeys = new Map()
}
begin (guildId, bundleId) {
if (!guildId || !bundleId) {
return { resume: false, completedPhases: new Set(), bundleId: null }
}
const prev = this.byGuild.get(guildId)
if (prev?.bundleId === bundleId && prev.partial) {
return {
resume: true,
completedPhases: new Set(prev.completedPhases || []),
bundleId
}
}
this.byGuild.set(guildId, {
bundleId,
partial: true,
completedPhases: [],
startedAt: Date.now()
})
this.appliedMessageKeys.set(bundleId, new Set())
return { resume: false, completedPhases: new Set(), bundleId }
}
shouldRunPhase (completedPhases, phase) {
return !completedPhases.has(phase)
}
markPhase (guildId, bundleId, phase) {
const row = this.byGuild.get(guildId)
if (!row || row.bundleId !== bundleId) return
if (!row.completedPhases.includes(phase)) row.completedPhases.push(phase)
}
finish (guildId, bundleId) {
const row = this.byGuild.get(guildId)
if (row?.bundleId === bundleId) {
row.partial = false
row.finishedAt = Date.now()
}
if (bundleId) this.appliedMessageKeys.delete(bundleId)
}
hasAppliedMessage (bundleId, channelId, messageId) {
const set = this.appliedMessageKeys.get(bundleId)
if (!set) return false
return set.has(`${channelId}:${messageId}`)
}
noteAppliedMessage (bundleId, channelId, messageId) {
if (!bundleId || !channelId || !messageId) return
let set = this.appliedMessageKeys.get(bundleId)
if (!set) {
set = new Set()
this.appliedMessageKeys.set(bundleId, set)
}
set.add(`${channelId}:${messageId}`)
}
snapshotForDiagnostics (guildId) {
const row = this.byGuild.get(guildId)
if (!row) return null
return {
bundleId: row.bundleId,
partial: row.partial,
completedPhases: [...(row.completedPhases || [])],
startedAt: row.startedAt,
finishedAt: row.finishedAt || null
}
}
}
const guildSyncReplay = new GuildSyncReplayTracker()
module.exports = {
GUILD_SYNC_INGEST_PHASES,
deriveSyncBundleId,
guildSyncReplay
}