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]>
943 lines
36 KiB
JavaScript
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 }
|