Files
pearcord-platform/platform-pearcord-platform-class.js
T
Raven ScottandCursor 66160bbc53 Improve guild member roster sync and mesh recovery reliability.
Queue member join and sync-request gossip through the outbox, infer members from mesh peers and profiles, add host-initiated identify handshake, and harden partition heal plus mesh bounded clearing.

Co-authored-by: Cursor <[email protected]>
2026-06-03 20:28:00 -04:00

943 lines
36 KiB
JavaScript

'use strict'
const {
fs, path, b4a, EventEmitter,
swarmScope, dmScope, appEventsScope, automationScope,
deliveryScope, integrationScope, botAuthScope, cryptoScope,
threadsScope, searchScope, channelMetaScope, localScope,
foldersScope, enrichmentScope, sharedScope, permissionsScope,
hyperdbScope, createLogger, LocalDatabase, PearcordIdentity,
resolveStoragePath, writeOnboardedMarker, PearcordGuild, GuildReplicator,
GuildSidecarStore, PearcordMessage, PearcordPresence, PearcordInvite,
DmVideoFramePublisher, PearcordAttachments, PearcordVoiceNotes, canSendVoiceNote,
PearcordModeration, PearcordVoice, VoiceMediaHub, PearcordDmVoice,
VoiceCaptureFeed, PearcordSlashRegistry, parseSlashInvocation, PearcordEmojiRegistry,
buildEmojiSlotSnapshot, BotEventFanout, BOT_EVENTS, exportBodyFromSlice,
PearcordIntegrationWorker, INTENTS, BotRateLimits, PearcordStepUp,
PearcordForum, PearcordStage, STAGE_ROLE, isStageChannel,
PearcordAutomod, PearcordWebhooks, ScreenShareHub, broadcastScreenMedia,
DisplayCaptureFeed, PearcordAnnouncements, formatCrosspostBody, PearcordBoosts,
MAX_BOOSTS, MAX_SLOTS_PER_USER, PearcordStickerRegistry, PearcordSoundboardRegistry,
decodeSoundToPcm, ALLOWED_MIMES, PearcordAnnouncementHub, formatRemoteHubBody,
buildHubMirrorCard, formatHubMirrorFallback, PearcordContacts, PearcordNotifications,
PearcordSettings, UserPrefsMirrorStore, DmCallHistoryStore, PearcordProfileCosmetics,
enrichProfileCosmeticGossip, enrichGuildUpdateGossip, pickBannerHash, PearcordDeviceSync,
PearcordDiscovery, collectDiscoveryTags, getListingTtlMs, getGuildDrive,
isHexDriveKey, fetchFromProvider, PearcordEmbed, forumPostHaystack,
buildActivityFeed, enrichActivityWithAttachments, IPC_CONTRACT_VERSION, Hyperswarm,
id, now, guildTopic
} = require('./platform-class-imports')
class PearcordPlatform extends EventEmitter {
constructor (opts = {}) {
super()
this.storagePath = opts.storagePath || resolveStoragePath()
this._hyperswarmDht = opts.hyperswarmDht || null
this.log = createLogger('platform')
this.dbPath = path.join(this.storagePath, 'db')
this.db = new LocalDatabase(this.dbPath)
this.identity = new PearcordIdentity({ db: this.db, dbPath: this.dbPath, storagePath: this.storagePath })
this.guild = null
this.messages = null
this.presence = null
this.invite = null
this.dm = null
this.dmVoice = null
this._dmVoiceMedia = null
this._dmScreenShare = null
this._dmVideoPublisher = null
this._dmVideoTilesByUser = new Map()
this._dmScreenFrameBroadcastBound = false
this._prefsMirrorStore = null
this._callHistoryStore = null
this._dmCallHistoryLoaded = false
this.attachments = null
this.moderation = null
this.voice = null
this.voiceMedia = null
this.voiceCapture = null
this.screenShare = null
this.displayCapture = null
this.announcements = null
this.announcementHub = null
this.boosts = null
this.slashCommands = null
this.emojiRegistry = null
this.stickerRegistry = null
this.soundboardRegistry = null
this.guildRoles = null
this._activeChannelOpenLastReadAt = 0
this._dmSessionRekeyNotice = null
this._dmCallHistory = []
this._dmComposerPrefill = null
this.channelPermissions = null
this._recentSoundPlays = []
this.botEvents = null
this.appEvents = null
this.automationExport = null
this.webhooks = null
this.deliveryReceipts = null
this.hookDispatcher = null
this.hookFailureAlerts = null
this.workerReleaseHints = null
this._auditScheduleTimer = null
this._dmScheduledMessageTimer = null
this._auditArchiveFetchWaiters = new Map()
this._automationDigestSnapshotFetchWaiters = new Map()
this.appVersion = opts.appVersion || process.env.PEARCORD_APP_VERSION || '0.0.0'
this.discovery = null
this.contacts = null
this.botLimits = null
this.notifications = null
this.userSettings = null
this.profileCosmetics = null
this._peerCosmetics = new Map()
this.deviceSync = null
this.embeds = null
this._discoverySwarm = null
this._contactsSwarm = null
this._settingsSwarm = null
this._settingsMeshWired = false
this._settingsHealthChannels = new Map()
this._guildSidecarStores = new Map()
this._settingsMeshHealthByDevice = new Map()
this._settingsMeshHealthLastAt = 0
this._settingsMeshHealthGossipTimer = null
this._settingsPresenceGossipTimer = null
this._devicePairSwarm = null
this._discoveryWired = false
this._archivePeerHealthRefreshedAt = null
this._lastComplianceSnapshot = null
this._lastArchivePeerExport = null
this._archiveHealthRefreshTimer = null
this.mode = 'home'
this.activeChannelId = null
this.onboarded = false
this.guilds = []
this._searchQuery = ''
this._searchScope = 'channel'
this._searchIncludeMesh = true
this._searchUnchanged = false
this._lastSearchMeshMeta = null
this._guildSearchIndexes = new Map()
this._searchIndexStore = null
this._searchIndexPersistTimers = new Map()
this._searchMeshWaiters = new Map()
this._threadsPanelFilter = 'active'
this._threadsPanelParentFilter = null
this._threadsPanelSort = 'activity'
this._discoveryMeshRefreshAt = 0
this._discoverySwarmRefreshAt = 0
this._discoveryMeshHealTimer = null
this._discoveryListingRefreshByGuild = new Map()
this._federationHubIngestStats = null
this._discoveryJoinSyncPending = false
this._discoveryPostJoinRefreshAt = 0
this._discoveryDiffHint = null
this._discoveryDiffUntil = 0
this._discoveryListingIds = new Set()
this._lastDiscoveryMeshIngest = null
this._voiceSpeakingMesh = {}
this._voiceMuteMesh = {}
this._discoveryExploreBaseline = null
this._discoveryExploreBaselineAt = null
this._voiceRosterMutedOnly = false
this._voiceRosterDeafenedOnly = false
this._voiceRosterExpandedChannelIds = new Set()
this._onboardingExplorePending = false
this._lastVoiceSpeakingGossipKey = ''
this._lastVoiceSpeakingGossipAt = 0
this._readOnly = opts.readOnly === true
this._companionMode = opts.companionMode === true || opts.readOnly === true
/** @type {Set<string>} */
this._attachmentDownloadCancel = new Set()
this._discoveryFilter = { query: '', sort: 'newest', minMembers: 0, tag: '' }
this._guildTyping = new Map()
this._typingTimers = new Set()
this._guildPresenceSummary = new Map()
this._guildPresencePrefsTimer = null
this._searchFetchedAt = 0
this._searchResolveCacheKey = ''
this._cachedSearchResults = null
this._cachedPublicListings = null
this._cachedPublicListingsAt = 0
this._cachedDiscoveryStats = null
this._cachedDiscoveryStatsAt = 0
this._discoveryViewCacheMs = Number(process.env.PEARCORD_DISCOVERY_VIEW_CACHE_MS) || 5000
this._searchCacheGuildId = ''
this._lastSearchFromCache = false
this._searchMeshStaleMs = 90000
this._sessionReady = false
this._sessionError = null
this._sessionStartupInFlight = null
this._guildLoading = false
this._guildLoadingGuildId = null
this._guildLoadGeneration = 0
this._guildMeshJoinCoalesce = null
this._guildDeleteInProgress = null
this._guildOpenNoViewableChannels = false
this._emojiAttachmentMissUntil = new Map()
this._stickerAttachmentMissUntil = new Map()
this._guildSyncHealthByGuild = new Map()
this._guildReplicators = new Map()
this._guildSyncAcks = new Map()
this._guildSyncPushHalted = new Set()
this._guildGossipOutbox = new swarmScope.GossipOutbox({ maxEntries: 256 })
this._guildTopicPool = new swarmScope.GuildTopicPool()
this._guildSyncRequestBackoffGen = 0
this._guildSyncPushCoalesceTimer = null
this._guildSyncRequestDebounce = new Map()
this._presenceVectorByGuild = new Map()
this._reactionTombstones = new Map()
this._sparseOpenTimestamps = []
this._sparseAckCursorByGuild = new Map()
this._guildSyncBandwidthWindow = { at: 0, used: 0 }
this._guildSyncBandwidthDeferTimer = null
this._guildSyncBurstRttMs = 0
this._contactsPresenceDebounce = null
this._lastGuildSyncIngestStart = 0
this._guildSyncBandwidthPeerIndex = 0
this._reactionListCacheKey = ''
this._reactionListCache = null
this._reactionListCacheGen = 0
this._reactionListPending = null
this._reactionListMetrics = { cacheHit: 0, pendingHit: 0, miss: 0 }
this._pinListCacheKey = ''
this._pinListCache = null
this._pinListCacheGen = 0
this._pinListPending = null
this._pinListMetrics = { cacheHit: 0, pendingHit: 0, miss: 0 }
this._viewBuildStats = {
total: 0,
light: 0,
pinListSpanSkipped: 0,
reactionListSpanSkipped: 0
}
this._discoveryListPending = null
this._discoveryListMetrics = { cacheHit: 0, pendingHit: 0, miss: 0 }
this._discoveryListFrameDedupe = localScope.createDiscoveryListFrameDedupe()
this._pollDbReadBudget = localScope.createPollDbReadBudget()
this._topicMergeCounters = channelMetaScope.createTopicMergeCounters()
this._lastUnreadConsistencyAt = 0
this._guildSyncSeq = 0
this._markReadFanout = new localScope.MarkReadFanoutQueue({
log: this.log,
onFlush: (channelId, guildId) => this.markChannelRead(channelId, guildId)
})
this._lastReplicationCompact = null
this._memberPageBurstTimestamps = []
this._guildMeshBoundedByGuild = new Map()
this._guildMeshPeerRetryMetaByGuild = new Map()
this._guildMeshPeerSeenByGuild = new Map()
this._guildMeshStalePeerSweepTimer = null
this._guildMeshStalePeerEvictedTotalByGuild = new Map()
this._lastPartitionHeal = null
this._lastPartitionHealByGuild = new Map()
this._partitionHealInProgressByGuild = new Set()
this._partitionHealCooldownUntilByGuild = new Map()
this._channelReplicationDiagByGuild = new Map()
this._meshZeroPeerSinceByGuild = new Map()
this._meshAutoHealCheckTimer = null
this._gossipOutboxReplayByGuild = new Map()
this._lastGuildSyncChannelCoreKeys = []
this._forumIndexByGuild = new Map()
this._guildOpenDurationSamples = []
this._guildOpenFallbackThrottledUntil = 0
this._embedPrefetchCoalesce = new Map()
this._embedResolveLatencyByMessage = new Map()
/** Suppress composer-driven embed prefetch during IME composition (Phase 666). */
this._composerImeComposing = false
this._composerEmbedPreview = null
this._embedGossipUrlHashes = new Set()
this._embedResolveRateByGuild = new Map()
this._dmEmbedGossipBytesByConversation = new Map()
/** Dedupe reaction gossip fanout by message+emoji+user (Phase 669). */
this._reactionGossipKeys = new Set()
/** Per-channel reaction toggle rate limit (Phase 669). */
this._reactionToggleRateByChannel = new Map()
/** Dedupe sticker gossip fanout (Phase 670). */
this._stickerGossipKeys = new Set()
/** Per-channel sticker send rate limit (Phase 670). */
this._stickerSendRateByChannel = new Map()
/** Per-channel GIF attachment send rate limit (Phase 670). */
this._gifSendRateByChannel = new Map()
/** Dedupe pin gossip fanout (Phase 671). */
this._pinGossipKeys = new Set()
/** Per-channel pin toggle rate limit (Phase 671). */
this._pinToggleRateByChannel = new Map()
/** Per-user thread create rate limit (Phase 672). */
this._threadCreateRateByUser = new Map()
/** Dedupe thread metadata gossip ingest (Phase 672). */
this._threadMetaGossipKeys = new Set()
/** Dedupe presence gossip fanout (Phase 673). */
this._presenceGossipKeys = new Set()
/** Multi-select forum tag filters (Phase 672). */
this._forumTagFilters = []
}
async ready () {
const span = this.log.time('platform.ready')
try {
await this.identity.ready()
this.channelDescriptions = new channelMetaScope.ChannelDescriptionStore({ storagePath: this.storagePath })
await this.channelDescriptions.ready()
const snap = this.identity.snapshot()
this.onboarded = !!snap.user
if (this.onboarded) writeOnboardedMarker(this.storagePath)
if (snap.user) {
this.presence = new PearcordPresence({ userId: snap.user.id })
this.invite = new PearcordInvite({ db: this.db, creatorId: snap.user.id })
await this.invite.ready()
this.attachments = new PearcordAttachments({ storagePath: this.storagePath })
await this.attachments.ready()
this.voiceNotes = new PearcordVoiceNotes({ storagePath: this.storagePath })
await this.voiceNotes.ready()
this.moderation = new PearcordModeration({ storagePath: this.storagePath })
await this.moderation.ready()
this.discovery = new PearcordDiscovery({ storagePath: this.storagePath })
await this.discovery.ready()
this._wireDiscoveryEvents()
this.embeds = new PearcordEmbed({ storagePath: this.storagePath })
await this.embeds.ready()
this.botLimits = new BotRateLimits({ storagePath: this.storagePath })
await this.botLimits.ready()
if (snap.user) {
this.notifications = new PearcordNotifications({
storagePath: this.storagePath,
userId: snap.user.id
})
await this.notifications.ready()
this.notifications.on('notification', (n) => this.emit('notification', n))
this.notifications.on('prefs', () => this.emit('notification-prefs'))
this.userSettings = new PearcordSettings({
storagePath: this.storagePath,
userId: snap.user.id
})
this.userSettings._prefsMirror = await this._ensurePrefsMirrorStore()
await this.userSettings.ready()
const prefsRecovery = await this.userSettings.ensurePrefsRecoveredOnStartup()
if (prefsRecovery?.recovered) {
this.log.info('userPrefs startup recovery', {
spanKind: 'userPrefs.startup-recovery',
userId: snap.user.id,
source: prefsRecovery.source,
reason: prefsRecovery.reason || null,
backupUsed: !!prefsRecovery.backupUsed,
issueCount: (prefsRecovery.issues || []).length
})
}
const sessionPrefs = await this.userSettings.getPrefs()
this._ingestGuildPresenceCacheFromPrefs(sessionPrefs)
this._ingestDiscoveryExploreBaselineFromPrefs(sessionPrefs)
this._applyGlobalPresenceMeshFromPrefs(sessionPrefs)
this.userSettings.on('prefs', (prefs) => {
this._ingestGuildPresenceCacheFromPrefs(prefs)
this._ingestDiscoveryExploreBaselineFromPrefs(prefs)
this._applyGlobalPresenceMeshFromPrefs(prefs)
this.emit('user-prefs')
})
this.profileCosmetics = new PearcordProfileCosmetics({
storagePath: this.storagePath,
userId: snap.user.id
})
await this.profileCosmetics.ready()
this.profileCosmetics.on('cosmetics', () => this.emit('profile-cosmetics'))
this.profileCosmetics.on('peer-cosmetics', (p) => this._ingestPeerCosmetics(p))
}
this.guilds = await this.listGuilds()
await this._applyPendingGuildDeletes()
this._startDmScheduledMessageTimer()
this.log.info('platform ready', { guilds: this.guilds.length, user: snap.user?.id })
void this._finishSessionStartup()
}
span.end({ onboarded: this.onboarded })
return this
} catch (err) {
span.fail(err)
throw err
}
}
async _joinSessionStartupMeshes () {
const meshMs = Number(process.env.PEARCORD_SESSION_MESH_MS) || 12000
await this._startupStepTimeout(
Promise.all([
this._joinDiscoveryMesh(),
this._joinContactsMesh(),
this._joinSettingsMesh()
]),
meshMs,
'session-startup-meshes'
)
}
_markSessionReady () {
if (this._sessionReady) return
this._sessionReady = true
this.log.info('session ready', { sessionError: this._sessionError || null })
this.emit('session-ready')
}
/** Guild mesh + discovery/contacts — mesh joins are capped; UI unblocks on sessionReady. */
async _finishSessionStartup () {
if (this._sessionStartupInFlight) return this._sessionStartupInFlight
this._sessionStartupInFlight = this._finishSessionStartupInner().finally(() => {
this._sessionStartupInFlight = null
})
return this._sessionStartupInFlight
}
async _finishSessionStartupInner () {
const span = this.log.time('session.startup')
const capMs = Number(process.env.PEARCORD_SESSION_STARTUP_MS) || 28000
let capped = false
const capTimer = setTimeout(() => {
if (this._sessionReady) return
capped = true
this._sessionError =
this._sessionError ||
`Session startup timed out after ${capMs}ms — continuing offline`
this.log.warn('session.startup capped', { capMs })
this._markSessionReady()
}, capMs)
try {
if (this.guilds.length && !this.guild && this.mode !== 'dm') {
let startGuildId = this.guilds[0].id
if (this._companionMode && this.userSettings) {
const prefs = await this.userSettings.getPrefs()
const saved = prefs.companionLastGuildId
if (saved && this.guilds.some((g) => g.id === saved)) startGuildId = saved
}
this.log.info('session loading initial guild', { guildId: startGuildId })
const guildLoadMs = Number(process.env.PEARCORD_GUILD_LOAD_MS) || 15000
let guildLoadTimedOut = false
await Promise.race([
this.loadGuild(startGuildId),
new Promise((resolve) => {
setTimeout(() => {
guildLoadTimedOut = true
resolve()
}, guildLoadMs)
})
])
if (guildLoadTimedOut && this._guildLoading) {
this._guildLoadGeneration++
this._abortGuildMeshJoinCoalesce('session-startup-guild-timeout')
this._guildLoading = false
this._guildLoadingGuildId = null
this.emit('guild-loading')
const msg = `Guild load timed out after ${guildLoadMs}ms`
this._sessionError = this._sessionError || msg
this.log.warn('session initial guild load timed out', {
guildId: startGuildId,
guildLoadMs
})
}
}
void this._joinSessionStartupMeshes().catch((err) => {
this.log.warn('session startup meshes background failed', {
err: err?.message || String(err)
})
})
await this._applyPendingGuildDeletes()
if (this.discovery) {
setTimeout(() => {
this.discovery.syncGuildDeletesToMesh().catch(() => {})
}, 1200)
this._startDiscoveryMeshHealLoop()
}
const dmMs = Number(process.env.PEARCORD_SESSION_DM_MS) || 10000
await this._startupStepTimeout(
this._ensureDmBackgroundListener(),
dmMs,
'session-dm-background'
)
const deviceMs = Number(process.env.PEARCORD_SESSION_DEVICE_MS) || 8000
await this._startupStepTimeout(this._initDeviceSync(), deviceMs, 'session-device-sync')
const activeGid = this.guild?.guild?.id
const startupGuildMeshBounded = activeGid ? this._isGuildMeshBounded(activeGid) : false
span.end({
logLevel: startupGuildMeshBounded ? 'info' : undefined,
guildMeshBounded: startupGuildMeshBounded,
guildCount: this.guilds?.length || 0,
guildId: this.guild?.guild?.id || null,
activeChannelId: this.activeChannelId || null,
spanKind: 'session.startup',
capped
})
} catch (err) {
this._sessionError = err?.message || String(err)
this.log.error('session.startup error', {
err: err?.message || String(err)
})
this.log.error('session startup failed', { err })
this.emit('session-error', err)
span.fail(err)
} finally {
clearTimeout(capTimer)
this._markSessionReady()
}
}
async _initDeviceSync () {
if (!this.onboarded || !this.identity?.user) return
if (this.deviceSync) return
const snap = this.identity.snapshot()
this.deviceSync = new PearcordDeviceSync({
storagePath: this.storagePath,
userId: snap.user.id,
username: snap.user.username,
displayName: snap.user.displayName,
getIdentityBundle: async () => {
const b = this.identity.getIdentityBundle()
if (!b) return null
const devices = await this.deviceSync.listDevices()
return { ...b, devices }
}
})
await this.deviceSync.ready()
const devices = await this.deviceSync.listDevices()
if (!devices.length) {
await this.deviceSync.registerDevice({
deviceId: this.identity.deviceId,
label: 'Primary device',
isPrimary: true
})
}
}
async _bootstrapAfterUser (user, { waitSessionStartup = false } = {}) {
this.onboarded = true
writeOnboardedMarker(this.storagePath)
this.presence = new PearcordPresence({ userId: user.id })
this.invite = new PearcordInvite({ db: this.db, creatorId: user.id })
this.attachments = new PearcordAttachments({ storagePath: this.storagePath })
this.voiceNotes = new PearcordVoiceNotes({ storagePath: this.storagePath })
this.moderation = new PearcordModeration({ storagePath: this.storagePath })
this.discovery = new PearcordDiscovery({ storagePath: this.storagePath })
this.embeds = new PearcordEmbed({ storagePath: this.storagePath })
this.botLimits = new BotRateLimits({ storagePath: this.storagePath })
this.notifications = new PearcordNotifications({
storagePath: this.storagePath,
userId: user.id
})
this.userSettings = new PearcordSettings({
storagePath: this.storagePath,
userId: user.id,
prefsMirror: null
})
const prefsMirror = await this._ensurePrefsMirrorStore()
this.userSettings._prefsMirror = prefsMirror
this.profileCosmetics = new PearcordProfileCosmetics({
storagePath: this.storagePath,
userId: user.id
})
this.stepUp = new PearcordStepUp({
storagePath: this.storagePath,
userId: user.id
})
this.forum = new PearcordForum({ storagePath: this.storagePath })
this.stage = new PearcordStage({ storagePath: this.storagePath })
this.automod = new PearcordAutomod({ storagePath: this.storagePath })
this.announcements = new PearcordAnnouncements({ storagePath: this.storagePath })
this.announcementHub = new PearcordAnnouncementHub({ storagePath: this.storagePath })
this.boosts = new PearcordBoosts({ storagePath: this.storagePath })
await Promise.all([
this.invite.ready(),
this.attachments.ready(),
this.voiceNotes.ready(),
this.moderation.ready(),
this.discovery.ready(),
this.embeds.ready(),
this.botLimits.ready(),
this.notifications.ready(),
this.userSettings.ready(),
this.profileCosmetics.ready(),
this.stepUp.ready(),
this.forum.ready(),
this.stage.ready(),
this.automod.ready(),
this.announcements.ready(),
this.announcementHub.ready(),
this.boosts.ready()
])
this._wireDiscoveryEvents()
this.notifications.on('notification', (n) => this.emit('notification', n))
this.notifications.on('prefs', () => this.emit('notification-prefs'))
const prefsRecovery = await this.userSettings.ensurePrefsRecoveredOnStartup()
if (prefsRecovery?.recovered) {
this.log.info('userPrefs startup recovery', {
spanKind: 'userPrefs.startup-recovery',
userId: user.id,
source: prefsRecovery.source,
reason: prefsRecovery.reason || null,
backupUsed: !!prefsRecovery.backupUsed,
issueCount: (prefsRecovery.issues || []).length
})
}
const bootPrefs = await this.userSettings.getPrefs()
this._ingestGuildPresenceCacheFromPrefs(bootPrefs)
this._ingestDiscoveryExploreBaselineFromPrefs(bootPrefs)
this._applyGlobalPresenceMeshFromPrefs(bootPrefs)
this.userSettings.on('prefs', (prefs) => {
this._ingestGuildPresenceCacheFromPrefs(prefs)
this._ingestDiscoveryExploreBaselineFromPrefs(prefs)
this._applyGlobalPresenceMeshFromPrefs(prefs)
this.emit('user-prefs')
})
this.profileCosmetics.on('cosmetics', () => this.emit('profile-cosmetics'))
this.profileCosmetics.on('peer-cosmetics', (p) => this._ingestPeerCosmetics(p))
this._forumTagFilter = null
this._forumTagFilters = []
this._forumIncludeArchived = false
this._threadsPanelIncludeArchived = false
this._threadsPanelFilter = 'active'
this._threadsPanelParentFilter = null
this._threadsPanelSort = 'activity'
this._discoveryMeshRefreshAt = 0
this._discoverySwarmRefreshAt = 0
this._discoveryMeshHealTimer = null
this._discoveryListingRefreshByGuild = new Map()
this._federationHubIngestStats = null
this._discoveryJoinSyncPending = false
this._discoveryPostJoinRefreshAt = 0
this._discoveryDiffHint = null
this._discoveryDiffUntil = 0
this._discoveryListingIds = new Set()
this._voiceSpeakingMesh = {}
this._voiceMuteMesh = {}
this._discoveryExploreBaseline = null
this._discoveryExploreBaselineAt = null
this._voiceRosterMutedOnly = false
this._voiceRosterDeafenedOnly = false
this._onboardingExplorePending = false
this._lastVoiceSpeakingGossipKey = ''
this._lastVoiceSpeakingGossipAt = 0
this.guilds = await this.listGuilds()
const startup = this._finishSessionStartup()
if (waitSessionStartup) await startup
else void startup
this.emit('user', user)
return user
}
async _leaveMeshes () {
this._abortGuildMeshJoinCoalesce('leave-meshes')
this._stopGuildMeshStalePeerSweep()
this._stopAuditExportScheduleTimer()
this._stopDmScheduledMessageTimer()
await this._leaveVoice({ gossip: true })
if (this.guild) await this.guild.leaveMesh().catch(() => {})
if (this.dm && this.dm !== this._dmBg) {
await this.dm.leaveMesh().catch(() => {})
}
}
async _leaveVoice ({ gossip = false } = {}) {
if (!this.voice) return null
const guild = this.guild?.guild
const userId = this.identity?.user?.id
let leavingChannelId = this.voice.myChannelId
if (!leavingChannelId && userId && guild) {
const rows = await this.voice.listByGuild()
const mine = rows.find((r) => r.userId === userId)
if (mine) {
leavingChannelId = mine.channelId
this.voice.myChannelId = mine.channelId
}
}
if (!leavingChannelId) return null
if (guild && this.stage && userId) {
const ch = await this.db.get(sharedScope.COLLECTIONS.CHANNELS, {
guildId: guild.id,
id: leavingChannelId
})
if (isStageChannel(ch)) {
await this.stage.clearUser(guild.id, leavingChannelId, userId)
if (gossip && this.guild) {
this.guild.gossipStageSpeaker({
guildId: guild.id,
channelId: leavingChannelId,
userId,
role: null,
cleared: true
})
}
}
}
const payload = await this.voice.leave()
this.displayCapture?.reset()
if (this.screenShare?.active) {
const screenStop = await this.screenShare.stopShare()
if (gossip && screenStop && this.guild) {
this.guild.gossipScreenShare(screenStop)
this.guild.sendScreenMedia({
op: 'screen-stop',
guildId: screenStop.guildId,
channelId: screenStop.channelId,
userId: screenStop.userId,
sessionId: screenStop.sessionId
})
}
}
if (this.voiceMedia) {
const mediaLeave = await this.voiceMedia.leave()
if (gossip && mediaLeave && this.guild) {
this.guild.gossipVoiceMediaLeave(mediaLeave)
this.guild.sendVoiceMedia(mediaLeave)
}
}
this.voiceCapture?.reset()
if (gossip && payload && this.guild) this.guild.gossipVoiceState(payload)
return payload
}
async openHome () {
this._invalidateSearchCache()
this._guildLoadGeneration++
this._guildLoading = false
this._guildLoadingGuildId = null
this.emit('guild-loading')
await this._leaveMeshes()
await this._closeDM()
this.guild = null
this.mode = 'home'
this.activeChannelId = null
return { mode: 'home' }
}
/** Ingest a guild message from mesh gossip (works while on home after join). */
_isGuildLoadCurrent (loadGen) {
return loadGen == null || loadGen === this._guildLoadGeneration
}
async _openGuild (guildRecord, loadGen = null) {
const openStartedAt = Date.now()
const fallbackThrottled = this._isGuildOpenFallbackThrottled()
const span = this.log.time('guild.open', {
guildId: guildRecord.id,
fallbackThrottled
})
try {
if (fallbackThrottled) {
this.log.info('guild.open fallback-throttled', {
spanKind: 'guild.open.fallback-throttle',
guildId: guildRecord.id,
remainingMs: this._guildOpenFallbackThrottleRemainingMs(),
slowCount: this._guildOpenSlowCountInWindow()
})
}
const leaveSpan = this.log.time('guild.open.leaveMeshes')
await this._leaveMeshes()
if (!this._isGuildLoadCurrent(loadGen)) {
span.end({ superseded: true, logLevel: 'info' })
return { superseded: true }
}
leaveSpan.end()
await this._closeDM()
this.activeChannelId = null
this._guildOpenNoViewableChannels = false
this.mode = 'guild'
const user = this.identity.user
this.guild = new PearcordGuild({
ownerId: guildRecord.ownerId,
userId: user.id,
db: this.db,
swarm: this._newHyperswarm(this.identity.keyPair)
})
await this.guild.ready()
this.guild.setActiveGuild(guildRecord)
this._wireGuild(this.guild)
this._setGuildMeshBounded(guildRecord.id, false)
this._logMultiGuildMesh('guild-open', { guildId: guildRecord.id })
const meshSpan = this.log.time('guild.open.joinMesh')
try {
await this._requestGuildMeshJoin({
source: 'guild-open',
loadGen,
bounded: true,
fallbackThrottled
})
} catch (err) {
this.log.error('guild.mesh error', {
guildId: guildRecord.id,
err: err?.message || String(err)
})
throw err
}
if (!this._isGuildLoadCurrent(loadGen)) {
meshSpan.end({ superseded: true, logLevel: 'info' })
await this._leaveMeshes()
span.end({ superseded: true, logLevel: 'info' })
return { superseded: true }
}
meshSpan.end({
peers: this.guild.peers?.size ?? 0,
logLevel: this._isGuildMeshBounded(guildRecord.id) ? 'info' : undefined
})
if (user.id === guildRecord.ownerId) {
void this._getGuildReplicator(guildRecord.id).catch(() => {})
}
this._scheduleProfileGossipBurst()
if ((this.guild.peers?.size ?? 0) === 0) {
this._scheduleGuildMeshPeerRetry(guildRecord.id)
} else {
this._maybeEvictStaleGuildMeshPeers(guildRecord.id, 'guild-open')
}
this._startGuildMeshStalePeerSweep(guildRecord.id)
const voiceSpan = this.log.time('guild.open.voice')
await this._initGuildVoice(guildRecord.id)
voiceSpan.end()
this.messages = new PearcordMessage({ authorId: user.id, db: this.db })
await this.messages.ready()
const channelsSpan = this.log.time('guild.open.listChannels')
const membersSpan = this.log.time('guild.open.listMembers')
const [channels, memberRows] = await Promise.all([
this.guild.listChannels(),
this.guild.listMembers()
])
channelsSpan.end({ count: channels.length })
membersSpan.end({ count: memberRows.length })
void memberRows
const pickSpan = this.log.time('guild.open.selectChannel')
await this._selectGuildChannelWithUnread(channels)
pickSpan.end({ activeChannelId: this.activeChannelId })
void this.discovery?.recordGuild(guildRecord).catch(() => {})
void this.discovery?.touchGuild(guildRecord.id).catch(() => {})
setTimeout(() => {
void this._ensureGuildModules().catch((err) => {
this.log.warn('guild modules init deferred failed', { err })
})
}, 0)
this._startAuditExportScheduleTimer()
if (!fallbackThrottled) {
void this._maybeArchiveHealthRefreshOnFocus().catch(() => {})
this._restartArchiveHealthRefreshTimer()
}
if (
this.activeChannelId &&
!this._guildOpenNoViewableChannels &&
!fallbackThrottled
) {
void this._prefetchEmojiAttachmentsAfterOpen().catch((err) => {
this.log.debug('emoji batch prefetch after guild open failed', { err })
})
}
if (guildRecord.ownerId !== user.id) {
const hostPk = await this._lookupGuildHostPublicKey(guildRecord)
const syncWaitMs = fallbackThrottled
? Math.max(
500,
Number(process.env.PEARCORD_GUILD_OPEN_THROTTLE_SYNC_WAIT_MS) || 2000
)
: 18000
void this._bootstrapGuildSessionAfterJoin({
invite: hostPk
? { creatorId: guildRecord.ownerId, creatorPublicKey: hostPk }
: null,
hostPublicKey: hostPk,
skipMeshJoin: true,
syncWaitMs
}).catch((err) => {
this.log.warn('guild.open member sync bootstrap failed', {
guildId: guildRecord.id,
err: err?.message || String(err)
})
})
if (!fallbackThrottled) {
this._scheduleGuildMemberSyncRecovery(guildRecord.id, hostPk)
}
}
const durationMs = Date.now() - openStartedAt
this._recordGuildOpenDuration(guildRecord.id, durationMs)
span.end({
guildId: guildRecord.id,
channels: channels.length,
channelCount: channels.length,
memberCount: memberRows.length,
activeChannelId: this.activeChannelId,
guildCount: (this.guilds || []).length,
noViewableChannels: !!this._guildOpenNoViewableChannels,
durationMs,
fallbackThrottled,
openFallbackThrottleActive: this._isGuildOpenFallbackThrottled(),
spanKind: 'guild.open',
logLevel:
this._guildOpenNoViewableChannels ||
this._isGuildMeshBounded(guildRecord.id) ||
fallbackThrottled
? 'info'
: undefined
})
return { superseded: false }
} catch (err) {
this.log.error('guild.open error', {
guildId: guildRecord.id,
err: err?.message || String(err)
})
span.fail(err)
throw err
}
}
async close () {
await this._leaveVoice({ gossip: false })
await this._leaveMeshes()
await this._closeDM()
if (this.discovery) {
await this.discovery.leaveMesh().catch(() => {})
}
if (this.contacts) {
await this.contacts.leaveMesh().catch(() => {})
this.contacts = null
}
if (this.notifications) {
await this.notifications.leaveMesh().catch(() => {})
}
if (this.userSettings) {
await this.userSettings.leaveMesh().catch(() => {})
}
if (this.profileCosmetics) {
await this.profileCosmetics.leaveMesh().catch(() => {})
}
if (this._contactsSwarm) {
await this._contactsSwarm.destroy().catch(() => {})
this._contactsSwarm = null
}
if (this._settingsSwarm) {
await this._settingsSwarm.destroy().catch(() => {})
this._settingsSwarm = null
this._settingsMeshWired = false
this._settingsHealthChannels.clear()
}
if (this._discoverySwarm) {
await this._discoverySwarm.destroy().catch(() => {})
this._discoverySwarm = null
}
if (this._devicePairSwarm) {
await this._devicePairSwarm.destroy().catch(() => {})
this._devicePairSwarm = null
}
this.deviceSync = null
if (this.guild) {
await this.guild.close()
this.guild = null
}
this.messages = null
this.voice = null
this.voiceMedia = null
this.voiceCapture = null
this.screenShare = null
this.displayCapture = null
this.announcements = null
this.announcementHub = null
this.boosts = null
this.removeAllListeners()
await this.db.close()
}
}
module.exports = { PearcordPlatform }