feat(platform): Phase 130 search refresh and presence prefs sync (v0.8.95)

Add refreshSearch() to re-run guild/forum mesh queries and stamp
searchMeshMeta.fetchedAt for stale UI indicators.

Persist guild online counts to USER_PREFS.guildPresenceCache (debounced
gossip) and merge remote cache on prefs ingest so companion rail presence
stays consistent across linked devices.

Expose presence row timestamps on guildPresenceSummary in view().

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-22 10:57:00 -04:00
co-authored by Cursor
parent be400a043a
commit 12360f4288
+65 -5
View File
@@ -330,6 +330,8 @@ class PearcordPlatform extends EventEmitter {
this._guildTyping = new Map() this._guildTyping = new Map()
this._typingTimers = new Set() this._typingTimers = new Set()
this._guildPresenceSummary = new Map() this._guildPresenceSummary = new Map()
this._guildPresencePrefsTimer = null
this._searchFetchedAt = 0
} }
async ready () { async ready () {
@@ -366,7 +368,12 @@ class PearcordPlatform extends EventEmitter {
userId: snap.user.id userId: snap.user.id
}) })
await this.userSettings.ready() await this.userSettings.ready()
this.userSettings.on('prefs', () => this.emit('user-prefs')) const sessionPrefs = await this.userSettings.getPrefs()
this._ingestGuildPresenceCacheFromPrefs(sessionPrefs)
this.userSettings.on('prefs', (prefs) => {
this._ingestGuildPresenceCacheFromPrefs(prefs)
this.emit('user-prefs')
})
this.profileCosmetics = new PearcordProfileCosmetics({ this.profileCosmetics = new PearcordProfileCosmetics({
storagePath: this.storagePath, storagePath: this.storagePath,
userId: snap.user.id userId: snap.user.id
@@ -458,7 +465,12 @@ class PearcordPlatform extends EventEmitter {
userId: user.id userId: user.id
}) })
await this.userSettings.ready() await this.userSettings.ready()
this.userSettings.on('prefs', () => this.emit('user-prefs')) const bootPrefs = await this.userSettings.getPrefs()
this._ingestGuildPresenceCacheFromPrefs(bootPrefs)
this.userSettings.on('prefs', (prefs) => {
this._ingestGuildPresenceCacheFromPrefs(prefs)
this.emit('user-prefs')
})
this.profileCosmetics = new PearcordProfileCosmetics({ this.profileCosmetics = new PearcordProfileCosmetics({
storagePath: this.storagePath, storagePath: this.storagePath,
userId: user.id userId: user.id
@@ -1043,6 +1055,17 @@ class PearcordPlatform extends EventEmitter {
} }
} }
async refreshSearch () {
if (!this._searchQuery || !this.guild?.guild?.id) {
return { refreshed: false, meta: this._lastSearchMeshMeta }
}
if (this._lastSearchMeshMeta) {
this._lastSearchMeshMeta = { ...this._lastSearchMeshMeta, refreshing: true }
}
await this._resolveSearchResults()
return { refreshed: true, meta: this._lastSearchMeshMeta }
}
async _resolveSearchResults () { async _resolveSearchResults () {
const q = this._searchQuery const q = this._searchQuery
if (!q || !this.guild?.guild?.id) return [] if (!q || !this.guild?.guild?.id) return []
@@ -1061,8 +1084,11 @@ class PearcordPlatform extends EventEmitter {
mesh: out.mesh, mesh: out.mesh,
meshHitCount: out.meshHitCount || 0, meshHitCount: out.meshHitCount || 0,
localHitCount: out.localHitCount || 0, localHitCount: out.localHitCount || 0,
scope: 'forum' scope: 'forum',
fetchedAt: Date.now(),
refreshing: false
} }
this._searchFetchedAt = this._lastSearchMeshMeta.fetchedAt
return out.hits return out.hits
} }
if (this._searchScope === 'guild') { if (this._searchScope === 'guild') {
@@ -1073,8 +1099,12 @@ class PearcordPlatform extends EventEmitter {
this._lastSearchMeshMeta = { this._lastSearchMeshMeta = {
mesh: out.mesh, mesh: out.mesh,
meshHitCount: out.meshHitCount || 0, meshHitCount: out.meshHitCount || 0,
localHitCount: out.localHitCount || 0 localHitCount: out.localHitCount || 0,
scope: 'guild',
fetchedAt: Date.now(),
refreshing: false
} }
this._searchFetchedAt = this._lastSearchMeshMeta.fetchedAt
return out.hits return out.hits
} }
return [] return []
@@ -4834,12 +4864,42 @@ class PearcordPlatform extends EventEmitter {
if (onlineish.has(p.status)) online += 1 if (onlineish.has(p.status)) online += 1
} }
this._guildPresenceSummary.set(guildId, { online, at: Date.now() }) this._guildPresenceSummary.set(guildId, { online, at: Date.now() })
this._scheduleGuildPresencePrefsSync()
}
_ingestGuildPresenceCacheFromPrefs (prefs) {
const cache = prefs?.guildPresenceCache
if (!cache || typeof cache !== 'object') return
for (const [gid, row] of Object.entries(cache)) {
if (!gid || !row) continue
const at = Number(row.at) || 0
const prev = this._guildPresenceSummary.get(gid)
if (!prev || at >= (prev.at || 0)) {
this._guildPresenceSummary.set(gid, {
online: Math.max(0, Number(row.online) || 0),
at
})
}
}
}
_scheduleGuildPresencePrefsSync () {
if (!this.userSettings) return
if (this._guildPresencePrefsTimer) clearTimeout(this._guildPresencePrefsTimer)
this._guildPresencePrefsTimer = setTimeout(() => {
this._guildPresencePrefsTimer = null
const cache = {}
for (const [gid, row] of this._guildPresenceSummary) {
cache[gid] = { online: row.online || 0, at: row.at || Date.now() }
}
void this.userSettings.setPrefs({ guildPresenceCache: cache }, { gossip: true }).catch(() => {})
}, 1200)
} }
_guildPresenceSummaryForView () { _guildPresenceSummaryForView () {
const out = {} const out = {}
for (const [gid, row] of this._guildPresenceSummary) { for (const [gid, row] of this._guildPresenceSummary) {
out[gid] = { online: row.online || 0 } out[gid] = { online: row.online || 0, at: row.at || 0 }
} }
if (this.guild?.guild?.id) { if (this.guild?.guild?.id) {
this._updateGuildPresenceSummary(this.guild.guild.id) this._updateGuildPresenceSummary(this.guild.guild.id)