Phase 748: extract thread/forum export and schedule mixins

Non-breaking refactor: move thread archive and forum tag palette export,
discovery listing push/revoke scheduling and listing delta hints, member page
debounce scheduling, guild sync attachment pull scheduling, and reaction
tombstone key helper out of platform-pearcord-platform-class.js. Manifest
rows 85→88; runtime registry 25→28. Sync and discovery scheduling unchanged.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-06-03 11:35:18 -04:00
co-authored by Cursor
parent 7d81dcf305
commit f7aeb84704
7 changed files with 213 additions and 172 deletions
+2
View File
@@ -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 748 (v0.8.724):** `platform-guild-thread-forum-export-mixin.js`, `platform-discovery-listing-schedule-mixin.js`, `platform-guild-member-sync-schedule-mixin.js` (88 mixin rows, 28 runtime mixins); thread/forum sync export slices, discovery listing push/revoke, member page and attachment pull scheduling. Bundle: `npm run test:ci-phase748`.
**Phase 747 (v0.8.723):** `platform-guild-sparse-sync-qos-mixin.js`, `platform-guild-contacts-presence-mixin.js`, `platform-voice-mesh-state-mixin.js` (85 mixin rows, 25 runtime mixins); sparse sync QoS, contacts presence reconcile, voice speaking/mute mesh gossip. Bundle: `npm run test:ci-phase747`.
**Phase 746 (v0.8.722):** `platform-guild-presence-vector-mixin.js`, `platform-settings-mesh-health-mixin.js`, `platform-guild-sync-wire-sparse-mixin.js` (82 mixin rows, 22 runtime mixins); presence vector sync, settings mesh health gossip, bundle wire sanitize, sparse open limits. Bundle: `npm run test:ci-phase746`.
@@ -0,0 +1,58 @@
'use strict'
const platformDiscoveryListingScheduleMixin = {
_scheduleDiscoveryListingPushForGuild (guildId, opts = {}) {
if (!guildId || !this.discovery) return
const pushOpts = { ...opts, force: opts.force !== false }
const delays = [0, 400, 1200, 2800, 6000, 12000, 20000]
for (const ms of delays) {
setTimeout(() => {
this._pushGuildListingToDiscoveryMesh(guildId, pushOpts).catch((err) => {
this.log.debug('discovery.listing push retry failed', {
guildId,
err: err?.message || String(err)
})
})
}, ms)
}
},
_scheduleDiscoveryRevokeBurst (guildId, payload = {}) {
if (!guildId || !this.discovery) return
const revoke = {
guildId,
id: guildId,
revokedAt: payload.revokedAt || Date.now()
}
const tomb = {
guildId,
deletedAt: payload.deletedAt || revoke.revokedAt,
deletedBy: payload.deletedBy || null,
name: payload.name || null
}
const delays = [0, 400, 1200, 2800, 6000]
for (const ms of delays) {
setTimeout(() => {
this.discovery.gossipListingRevoke(revoke).catch(() => {})
this.discovery.gossipGuildDelete(tomb).catch(() => {})
}, ms)
}
},
_noteDiscoveryListingDelta (row) {
if (!row?.id) {
this._discoveryDiffHint = 'Discovery listings updated'
this._discoveryDiffUntil = Date.now() + 8000
return
}
const isNew = !this._discoveryListingIds.has(row.id)
this._discoveryListingIds.add(row.id)
const name = row.name || 'Server'
this._discoveryDiffHint = isNew
? `New public listing: ${name}`
: `Listing updated: ${name}`
this._discoveryDiffUntil = Date.now() + 8000
}
}
module.exports = { platformDiscoveryListingScheduleMixin }
@@ -0,0 +1,92 @@
'use strict'
const platformGuildMemberSyncScheduleMixin = {
_reactionTombstoneKey (key, guildId) {
return `${guildId || ''}:${key.channelId}:${key.messageId}:${key.emoji}:${key.userId}`
},
_scheduleMemberPageRequest (offset = 0, delayMs = null) {
const envMs = Number(process.env.PEARCORD_GUILD_SYNC_MEMBER_PAGE_DEBOUNCE_MS)
const raw =
delayMs != null
? Number(delayMs) || 600
: Number.isFinite(envMs) && envMs > 0
? envMs
: 600
const ms = Math.max(50, raw)
if (this._memberPageDebounceTimer) clearTimeout(this._memberPageDebounceTimer)
this._pendingMemberPageOffset = Math.max(0, Number(offset) || 0)
this._memberPageDebounceTimer = setTimeout(() => {
this._memberPageDebounceTimer = null
const off = this._pendingMemberPageOffset
this._pendingMemberPageOffset = null
const span = this.log.time('guild.sync.member-page', {
spanKind: 'guild.sync.member-page',
guildId: this.guild?.guild?.id,
offset: off,
debounced: true
})
try {
this.requestGuildMemberPage(off, { debounced: true })
span.end({ offset: off })
} catch (err) {
span.fail(err)
}
}, ms)
this._patchGuildSyncHealth({
guildId: this.guild?.guild?.id,
memberPagePendingOffset: this._pendingMemberPageOffset,
memberPageDebounceMs: ms
})
return { offset: this._pendingMemberPageOffset, debounceMs: ms }
},
_scheduleGuildSyncAttachmentPulls (payload) {
const span = this.log.time('guild.sync.attach', {
spanKind: 'guild.sync.attach',
guildId: payload?.guildId || this.guild?.guild?.id,
count: payload?.attachments?.length || 0
})
const attachments = payload?.attachments || []
if (!attachments.length || !this.attachments) {
span.end({ scheduled: 0, skipped: attachments.length })
return { scheduled: 0, skipped: attachments.length, queueDepth: 0 }
}
const priorityCh = this.activeChannelId
const sorted = [...attachments].sort((a, b) => {
const aPri = a.channelId === priorityCh ? 0 : 1
const bPri = b.channelId === priorityCh ? 0 : 1
return aPri - bPri
})
const cap = Math.max(
1,
Number(process.env.PEARCORD_GUILD_ATTACHMENT_PULL_CAP) || 8
)
let scheduled = 0
for (const att of sorted) {
if (scheduled >= cap) break
if (!att?.id) continue
try {
if (this.attachments.hasLocalBytes(att)) continue
} catch {
/* ignore */
}
scheduled++
void this._prefetchAttachmentBytes(att, { label: 'guild-sync' }).catch(() => {})
}
const result = {
scheduled,
skipped: Math.max(0, attachments.length - scheduled),
queueDepth: Math.max(0, attachments.length - scheduled),
prioritizedChannelId: priorityCh || null
}
span.end(result)
this._patchGuildSyncHealth({
guildId: payload?.guildId || this.guild?.guild?.id,
attachmentPullQueueDepth: result.queueDepth
})
return result
}
}
module.exports = { platformGuildMemberSyncScheduleMixin }
@@ -0,0 +1,44 @@
'use strict'
const hyperdbScope = require('./platform-hyperdb-sync')
const platformGuildThreadForumExportMixin = {
_exportThreadArchiveSlice (channels = [], sinceTs = 0) {
const since = Number(sinceTs) || 0
return (channels || [])
.filter((c) => c.type === 'thread')
.slice(0, 96)
.map((c) => ({
channelId: c.id,
guildId: c.guildId,
archived: c.archived === true,
updatedAt: hyperdbScope.coerceSyncUint(c.updatedAt || c.createdAt, Date.now())
}))
.filter((r) => !since || (Number(r.updatedAt) || 0) > since)
},
_exportForumTagPalettesForSync (channels = [], sinceTs = 0) {
if (!this.forum?.parsePaletteFromTopic) return []
const since = Number(sinceTs) || 0
return (channels || [])
.filter((c) => c.type === 'forum')
.slice(0, 24)
.filter((c) => !since || (Number(c.updatedAt) || Number(c.createdAt) || 0) > since)
.map((c) => {
const tags = this.forum.parsePaletteFromTopic(c.topic)
const paletteVersion = hyperdbScope.coerceSyncUint(
tags.length ? tags.join(',').length + (Number(c.updatedAt) || 0) : 0,
1
)
return {
channelId: c.id,
guildId: c.guildId,
tags,
paletteVersion
}
})
.filter((r) => r.tags?.length || r.paletteVersion > 0)
}
}
module.exports = { platformGuildThreadForumExportMixin }
+4 -1
View File
@@ -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 = 85
const PLATFORM_MIXIN_ASSIGNMENT_COUNT = 88
const PLATFORM_MIXIN_ASSIGNMENTS = [
{ module: './polls-scheduling', export: 'pollSchedulingMixin' },
@@ -91,6 +91,9 @@ const PLATFORM_MIXIN_ASSIGNMENTS = [
{ module: './platform-guild-sparse-sync-qos-mixin', export: 'platformGuildSparseSyncQosMixin' },
{ module: './platform-guild-contacts-presence-mixin', export: 'platformGuildContactsPresenceMixin' },
{ module: './platform-voice-mesh-state-mixin', export: 'platformVoiceMeshStateMixin' },
{ module: './platform-guild-thread-forum-export-mixin', export: 'platformGuildThreadForumExportMixin' },
{ module: './platform-discovery-listing-schedule-mixin', export: 'platformDiscoveryListingScheduleMixin' },
{ module: './platform-guild-member-sync-schedule-mixin', export: 'platformGuildMemberSyncScheduleMixin' },
{ module: './platform-diagnostics-mixin', export: 'platformDiagnosticsMixin' }
]
+8 -169
View File
@@ -1241,9 +1241,7 @@ class PearcordPlatform extends EventEmitter {
_reactionTombstoneKey (key, guildId) {
return `${guildId || ''}:${key.channelId}:${key.messageId}:${key.emoji}:${key.userId}`
}
@@ -4438,43 +4436,9 @@ class PearcordPlatform extends EventEmitter {
this._cachedPublicListingsAt = 0
}
_scheduleDiscoveryListingPushForGuild (guildId, opts = {}) {
if (!guildId || !this.discovery) return
const pushOpts = { ...opts, force: opts.force !== false }
const delays = [0, 400, 1200, 2800, 6000, 12000, 20000]
for (const ms of delays) {
setTimeout(() => {
this._pushGuildListingToDiscoveryMesh(guildId, pushOpts).catch((err) => {
this.log.debug('discovery.listing push retry failed', {
guildId,
err: err?.message || String(err)
})
})
}, ms)
}
}
_scheduleDiscoveryRevokeBurst (guildId, payload = {}) {
if (!guildId || !this.discovery) return
const revoke = {
guildId,
id: guildId,
revokedAt: payload.revokedAt || Date.now()
}
const tomb = {
guildId,
deletedAt: payload.deletedAt || revoke.revokedAt,
deletedBy: payload.deletedBy || null,
name: payload.name || null
}
const delays = [0, 400, 1200, 2800, 6000]
for (const ms of delays) {
setTimeout(() => {
this.discovery.gossipListingRevoke(revoke).catch(() => {})
this.discovery.gossipGuildDelete(tomb).catch(() => {})
}, ms)
}
}
async _pushGuildListingToDiscoveryMesh (guildId, opts = {}) {
if (!this.discovery || !guildId) return null
@@ -4551,20 +4515,7 @@ class PearcordPlatform extends EventEmitter {
return listing
}
_noteDiscoveryListingDelta (row) {
if (!row?.id) {
this._discoveryDiffHint = 'Discovery listings updated'
this._discoveryDiffUntil = Date.now() + 8000
return
}
const isNew = !this._discoveryListingIds.has(row.id)
this._discoveryListingIds.add(row.id)
const name = row.name || 'Server'
this._discoveryDiffHint = isNew
? `New public listing: ${name}`
: `Listing updated: ${name}`
this._discoveryDiffUntil = Date.now() + 8000
}
/** Prune stale cache and re-broadcast listings on discovery mesh (Explore refresh). */
async refreshDiscoveryListingsFromMesh (opts = {}) {
@@ -5467,19 +5418,7 @@ class PearcordPlatform extends EventEmitter {
return applied
}
_exportThreadArchiveSlice (channels = [], sinceTs = 0) {
const since = Number(sinceTs) || 0
return (channels || [])
.filter((c) => c.type === 'thread')
.slice(0, 96)
.map((c) => ({
channelId: c.id,
guildId: c.guildId,
archived: c.archived === true,
updatedAt: hyperdbScope.coerceSyncUint(c.updatedAt || c.createdAt, Date.now())
}))
.filter((r) => !since || (Number(r.updatedAt) || 0) > since)
}
async _ingestThreadArchiveSlice (guildId, slice = []) {
if (!guildId || !slice?.length || !this.guild?.updateChannel) return 0
@@ -5516,28 +5455,7 @@ class PearcordPlatform extends EventEmitter {
return applied
}
_exportForumTagPalettesForSync (channels = [], sinceTs = 0) {
if (!this.forum?.parsePaletteFromTopic) return []
const since = Number(sinceTs) || 0
return (channels || [])
.filter((c) => c.type === 'forum')
.slice(0, 24)
.filter((c) => !since || (Number(c.updatedAt) || Number(c.createdAt) || 0) > since)
.map((c) => {
const tags = this.forum.parsePaletteFromTopic(c.topic)
const paletteVersion = hyperdbScope.coerceSyncUint(
tags.length ? tags.join(',').length + (Number(c.updatedAt) || 0) : 0,
1
)
return {
channelId: c.id,
guildId: c.guildId,
tags,
paletteVersion
}
})
.filter((r) => r.tags?.length || r.paletteVersion > 0)
}
async _ingestForumIndexBundle (guildId, rows = []) {
if (!guildId || !rows?.length) return 0
@@ -5588,88 +5506,9 @@ class PearcordPlatform extends EventEmitter {
return applied
}
_scheduleMemberPageRequest (offset = 0, delayMs = null) {
const envMs = Number(process.env.PEARCORD_GUILD_SYNC_MEMBER_PAGE_DEBOUNCE_MS)
const raw =
delayMs != null
? Number(delayMs) || 600
: Number.isFinite(envMs) && envMs > 0
? envMs
: 600
const ms = Math.max(50, raw)
if (this._memberPageDebounceTimer) clearTimeout(this._memberPageDebounceTimer)
this._pendingMemberPageOffset = Math.max(0, Number(offset) || 0)
this._memberPageDebounceTimer = setTimeout(() => {
this._memberPageDebounceTimer = null
const off = this._pendingMemberPageOffset
this._pendingMemberPageOffset = null
const span = this.log.time('guild.sync.member-page', {
spanKind: 'guild.sync.member-page',
guildId: this.guild?.guild?.id,
offset: off,
debounced: true
})
try {
this.requestGuildMemberPage(off, { debounced: true })
span.end({ offset: off })
} catch (err) {
span.fail(err)
}
}, ms)
this._patchGuildSyncHealth({
guildId: this.guild?.guild?.id,
memberPagePendingOffset: this._pendingMemberPageOffset,
memberPageDebounceMs: ms
})
return { offset: this._pendingMemberPageOffset, debounceMs: ms }
}
_scheduleGuildSyncAttachmentPulls (payload) {
const span = this.log.time('guild.sync.attach', {
spanKind: 'guild.sync.attach',
guildId: payload?.guildId || this.guild?.guild?.id,
count: payload?.attachments?.length || 0
})
const attachments = payload?.attachments || []
if (!attachments.length || !this.attachments) {
span.end({ scheduled: 0, skipped: attachments.length })
return { scheduled: 0, skipped: attachments.length, queueDepth: 0 }
}
const priorityCh = this.activeChannelId
const sorted = [...attachments].sort((a, b) => {
const aPri = a.channelId === priorityCh ? 0 : 1
const bPri = b.channelId === priorityCh ? 0 : 1
return aPri - bPri
})
const cap = Math.max(
1,
Number(process.env.PEARCORD_GUILD_ATTACHMENT_PULL_CAP) || 8
)
let scheduled = 0
for (const att of sorted) {
if (scheduled >= cap) break
if (!att?.id) continue
try {
if (this.attachments.hasLocalBytes(att)) continue
} catch {
/* ignore */
}
scheduled++
void this._prefetchAttachmentBytes(att, { label: 'guild-sync' }).catch(() => {})
}
const result = {
scheduled,
skipped: Math.max(0, attachments.length - scheduled),
queueDepth: Math.max(0, attachments.length - scheduled),
prioritizedChannelId: priorityCh || null
}
span.end(result)
this._patchGuildSyncHealth({
guildId: payload?.guildId || this.guild?.guild?.id,
attachmentPullQueueDepth: result.queueDepth
})
return result
}
requestGuildMemberPage (offset = 0, opts = {}) {
const guildId = this.guild?.guild?.id
+5 -2
View File
@@ -1,6 +1,6 @@
'use strict'
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738747). */
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738748). */
const PLATFORM_RUNTIME_MIXIN_MODULES = [
'./platform-hyperswarm-runtime-mixin',
'./platform-session-startup-mixin',
@@ -26,7 +26,10 @@ const PLATFORM_RUNTIME_MIXIN_MODULES = [
'./platform-guild-sync-wire-sparse-mixin',
'./platform-guild-sparse-sync-qos-mixin',
'./platform-guild-contacts-presence-mixin',
'./platform-voice-mesh-state-mixin'
'./platform-voice-mesh-state-mixin',
'./platform-guild-thread-forum-export-mixin',
'./platform-discovery-listing-schedule-mixin',
'./platform-guild-member-sync-schedule-mixin'
]
const PLATFORM_RUNTIME_MIXIN_COUNT = PLATFORM_RUNTIME_MIXIN_MODULES.length