'use strict' const automationMixin = { _automationHookGossipKeys: null, _automationRegistryHealWatermark: null, _digestRelayHealWatermark: null, _initAutomationMixinState () { if (!this._automationHookGossipKeys) { this._automationHookGossipKeys = new Set() } }, _shouldGossipAutomationHook (hook) { this._initAutomationMixinState() if (!hook?.guildId || !hook?.id) return true const hash = `${hook.guildId}:${hook.id}:${hook.name || ''}:${hook.enabled === false ? 0 : 1}:${hook.categories ?? ''}:${hook.action?.type || ''}:${JSON.stringify(hook.filter || {})}:${hook.updatedAt || 0}` if (this._automationHookGossipKeys.has(hash)) return false this._automationHookGossipKeys.add(hash) if (this._automationHookGossipKeys.size > 8192) { const first = this._automationHookGossipKeys.values().next().value if (first) this._automationHookGossipKeys.delete(first) } return true }, async _healAutomationRegistryOnPartition (guildId) { const gid = guildId || this.guild?.guild?.id || null const span = this.log.time('automation.heal', { spanKind: 'automation.heal', guildId: gid, slice: 'registry' }) try { if (!gid || !this.listAutomationHooks) { span.end({ relisted: 0, skipped: true }) return { relisted: 0, skipped: true } } const rows = await this.listAutomationHooks().catch(() => []) let relisted = 0 for (const row of rows) { if (row.guildId && row.guildId !== gid) continue if (this._shouldGossipAutomationHook(row)) relisted++ } if (relisted > 0 && this.guild?.gossipAutomationManifest) { this.guild.gossipAutomationManifest({ guildId: gid, manifest: { version: 1, guildId: gid, hooks: rows }, summary: { hooksImported: rows.length, importedAt: Date.now(), heal: true } }) } const watermark = Date.now() this._automationRegistryHealWatermark = watermark span.end({ relisted, watermark, guildAutomationCount: rows.length }) return { relisted, watermark, guildAutomationCount: rows.length } } catch (err) { this.log.error('automation.heal error', { guildId: gid, slice: 'registry', error: err?.message || String(err) }) span.fail(err) return { relisted: 0, error: err?.message || String(err) } } }, async _healDigestRelayCursorOnPartition (guildId) { const gid = guildId || this.guild?.guild?.id || null const span = this.log.time('automation.dispatch', { spanKind: 'automation.dispatch', guildId: gid, context: 'heal.relay' }) try { if (!gid) { span.end({ relisted: 0, skipped: true, guildAutomationCount: 0 }) return { relisted: 0, skipped: true } } const list = await this.listAutomationHooks().catch(() => []) const watermark = Date.now() this._digestRelayHealWatermark = watermark span.end({ relisted: 0, watermark, guildAutomationCount: list.length, enabledCount: list.filter((h) => h.enabled !== false).length, guildCount: (this.guilds || []).length, activeChannelId: this.activeChannelId || null, relayKind: 'automation.dispatch' }) return { relisted: 0, watermark, guildAutomationCount: list.length } } catch (err) { this.log.error('automation.dispatch error', { guildId: gid, context: 'heal.relay', error: err?.message || String(err) }) span.fail(err) return { relisted: 0, error: err?.message || String(err) } } } } module.exports = { automationMixin }