Phase 896: export mesh gossip-wait + permission helper extract (v0.8.863)

Non-breaking refactor — gossip-rpc-export-mesh-gossip-wait.js dedupes
gossip waiter/timeout orchestration; assertGuildExportMeshFetchPermission()
centralizes MANAGE_GUILD gate; audit/digest mesh fetch mixins adopt both
helpers. Default gossip v1 unchanged; fleet v2 cutover still deferred.

Verification: test:ci-phase896, pear run boot, pearcord.log scan.
This commit is contained in:
Raven Scott
2026-06-04 19:16:00 -04:00
parent 134dd522f9
commit c2b5f7cf2f
6 changed files with 134 additions and 90 deletions
@@ -1,13 +1,15 @@
'use strict'
const { deliveryScope, sharedScope, id } = require('../../../platform-class-imports')
const { deliveryScope } = require('../../../platform-class-imports')
const {
markRpcResponse,
fetchGuildGossipRpcThenMeshWait,
runGuildExportMeshFetchWithRetry
runGuildExportMeshFetchWithRetry,
assertGuildExportMeshFetchPermission,
awaitGuildExportMeshGossipWait
} = require('../../../gossip-rpc-pull')
/** Audit export archive mesh fetch + gossip waiters (Phase 895). */
/** Audit export archive mesh fetch + gossip waiters (Phase 895/896). */
const platformAuditExportMeshFetchMixin = {
_resolveAuditArchiveFetchWaiter (archive) {
if (!archive?.id || !archive.entries?.length) return
@@ -20,11 +22,10 @@ const platformAuditExportMeshFetchMixin = {
},
async fetchAuditExportArchiveFromMesh (archiveId, opts = {}) {
if (!this.guild?.guild) throw new Error('no guild')
const roles = await this._memberRoles()
if (!sharedScope.roleHasPermission(roles, sharedScope.PERMISSION.MANAGE_GUILD)) {
throw new Error('no permission to fetch audit export archive')
}
await assertGuildExportMeshFetchPermission(
this,
'no permission to fetch audit export archive'
)
return runGuildExportMeshFetchWithRetry(
(id, o) => this._fetchAuditExportArchiveFromMeshOnce(id, o),
archiveId,
@@ -55,43 +56,33 @@ const platformAuditExportMeshFetchMixin = {
fetchAttempt: (Number(opts.attempt) || 0) + 1
})
},
meshWait: async () => {
const timeoutMs = Math.min(12000, Math.max(800, Number(opts.timeoutMs) || 3500))
const requestId = id()
const waitPromise = new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this._auditArchiveFetchWaiters.delete(archiveId)
reject(new Error('audit archive fetch timed out'))
}, timeoutMs)
const handlers = this._auditArchiveFetchWaiters.get(archiveId) || []
handlers.push((arch) => {
clearTimeout(timer)
resolve(arch)
})
this._auditArchiveFetchWaiters.set(archiveId, handlers)
})
if (this.guild.gossipAuditExportArchiveRequest) {
this.guild.gossipAuditExportArchiveRequest({
guildId: this.guild.guild.id,
archiveId,
requestId,
requestedBy: this.identity.user?.id || null,
targetMemberId: opts.targetMemberId || null,
at: Date.now(),
attempt: Number(opts.attempt) || 0
})
meshWait: () => awaitGuildExportMeshGossipWait({
resourceId: archiveId,
opts,
waiters: this._auditArchiveFetchWaiters,
waiterMode: 'append',
timeoutErrorMessage: 'audit archive fetch timed out',
notFoundMessage: 'audit export archive not found on mesh',
sendGossipRequest: (requestId) => {
if (this.guild.gossipAuditExportArchiveRequest) {
this.guild.gossipAuditExportArchiveRequest({
guildId: this.guild.guild.id,
archiveId,
requestId,
requestedBy: this.identity.user?.id || null,
targetMemberId: opts.targetMemberId || null,
at: Date.now(),
attempt: Number(opts.attempt) || 0
})
}
},
refetchLocal: () => this.deliveryReceipts.getAuditExportArchive(archiveId),
validateLocal: (r) => Boolean(r?.entries?.length),
formatResult: async (fetched) => {
const out = await this.exportAuditExportArchive(fetched.id || archiveId, opts)
return { ...out, fetchAttempt: (Number(opts.attempt) || 0) + 1 }
}
const fetched = await waitPromise.catch(async () => {
row = await this.deliveryReceipts.getAuditExportArchive(archiveId)
if (row?.entries?.length) return row
throw new Error('audit export archive not found on mesh')
})
if (!fetched?.entries?.length) {
throw new Error('audit export archive not found on mesh')
}
const out = await this.exportAuditExportArchive(fetched.id || archiveId, opts)
return { ...out, fetchAttempt: (Number(opts.attempt) || 0) + 1 }
}
})
}
)
return rpcArch
@@ -1,13 +1,15 @@
'use strict'
const { deliveryScope, sharedScope, id } = require('../../../platform-class-imports')
const { deliveryScope } = require('../../../platform-class-imports')
const {
markRpcResponse,
fetchGuildGossipRpcThenMeshWait,
runGuildExportMeshFetchWithRetry
runGuildExportMeshFetchWithRetry,
assertGuildExportMeshFetchPermission,
awaitGuildExportMeshGossipWait
} = require('../../../gossip-rpc-pull')
/** Automation digest snapshot mesh fetch + gossip waiters (Phase 895). */
/** Automation digest snapshot mesh fetch + gossip waiters (Phase 895/896). */
const platformAutomationDigestMeshFetchMixin = {
_resolveAutomationDigestSnapshotFetchWaiter (snap) {
if (!snap?.id || !snap.body?.length) return
@@ -18,11 +20,10 @@ const platformAutomationDigestMeshFetchMixin = {
},
async fetchAutomationDigestSnapshotFromMesh (snapshotId, opts = {}) {
if (!this.guild?.guild) throw new Error('no guild')
const roles = await this._memberRoles()
if (!sharedScope.roleHasPermission(roles, sharedScope.PERMISSION.MANAGE_GUILD)) {
throw new Error('no permission to fetch automation digest snapshot')
}
await assertGuildExportMeshFetchPermission(
this,
'no permission to fetch automation digest snapshot'
)
return runGuildExportMeshFetchWithRetry(
(id, o) => this._fetchAutomationDigestSnapshotFromMeshOnce(id, o),
snapshotId,
@@ -62,48 +63,36 @@ const platformAutomationDigestMeshFetchMixin = {
fetchAttempt: (Number(opts.attempt) || 0) + 1
})
},
meshWait: async () => {
const timeoutMs = Math.min(12000, Math.max(800, Number(opts.timeoutMs) || 3500))
const requestId = id()
const waitPromise = new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this._automationDigestSnapshotFetchWaiters.delete(snapshotId)
reject(new Error('automation digest snapshot fetch timed out'))
}, timeoutMs)
this._automationDigestSnapshotFetchWaiters.set(snapshotId, [
(snap) => {
clearTimeout(timer)
resolve(snap)
}
])
})
if (this.guild.gossipAutomationDigestExportSnapshotRequest) {
this.guild.gossipAutomationDigestExportSnapshotRequest({
guildId: this.guild.guild.id,
snapshotId,
requestId,
requestedBy: this.identity.user?.id || null,
targetMemberId: opts.targetMemberId || null,
at: Date.now()
})
}
const fetched = await waitPromise.catch(async () => {
row = await this.deliveryReceipts.getAutomationDigestExportSnapshot(snapshotId)
if (row?.body?.length) return row
throw new Error('automation digest snapshot not found on mesh')
})
if (!fetched?.body?.length) {
throw new Error('automation digest snapshot not found on mesh')
}
return {
meshWait: () => awaitGuildExportMeshGossipWait({
resourceId: snapshotId,
opts,
waiters: this._automationDigestSnapshotFetchWaiters,
waiterMode: 'replace',
timeoutErrorMessage: 'automation digest snapshot fetch timed out',
notFoundMessage: 'automation digest snapshot not found on mesh',
sendGossipRequest: (requestId) => {
if (this.guild.gossipAutomationDigestExportSnapshotRequest) {
this.guild.gossipAutomationDigestExportSnapshotRequest({
guildId: this.guild.guild.id,
snapshotId,
requestId,
requestedBy: this.identity.user?.id || null,
targetMemberId: opts.targetMemberId || null,
at: Date.now()
})
}
},
refetchLocal: () => this.deliveryReceipts.getAutomationDigestExportSnapshot(snapshotId),
validateLocal: (r) => Boolean(r?.body?.length),
formatResult: (fetched) => ({
snapshotId: fetched.id,
format: fetched.format,
body: fetched.body,
summaryLines: fetched.summaryLines,
exportedAt: fetched.exportedAt,
fetchAttempt: (Number(opts.attempt) || 0) + 1
}
}
})
})
}
)
return rpcSnap