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:
@@ -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,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' }
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738–747). */
|
||||
/** Prototype mixins extracted from PearcordPlatform class body (Phases 738–748). */
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user