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]>
111 lines
3.1 KiB
JavaScript
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
|
|
}
|