Document that _selectGuildChannelWithUnread prefers lower channel.position when mention/unread scores tie; smoke test:guild-unread-position-tie. Co-authored-by: Cursor <[email protected]>
pearcord-platform
Application facade: one PearcordPlatform class that wires identity, database, guild mesh, DMs, invites, voice, discovery, bots, and dozens of feature modules for the Pearcord desktop sidecar and companion.
Mission
Expose a single EventEmitter API the UI drives over IPC (apps/pearcord/index.js), while owning session lifecycle (onboarding, guild/DM mode, active channel), permission checks, gossip bridging, and lazy initialization of feature registries (emoji, stickers, automation, search, …).
When to use / not
Use when:
- Building the Pearcord app, companion read-only client, or headless smoke that needs full session semantics.
- You want one import instead of composing
pearcord-identity+pearcord-guild+pearcord-messageyourself.
Do not use when:
- Writing a minimal library that only needs constants (
pearcord-shared) or raw DB access (pearcord-db). - Implementing a single feature in isolation — prefer the focused module (
pearcord-voice,pearcord-discovery, …) and optional thin adapter.
Public API
| Export | Role |
|---|---|
PearcordPlatform |
Main facade (~9k lines) |
USER_STATUS |
Re-exported from pearcord-shared for UI |
Session & identity
| Method / property | Role |
|---|---|
ready() |
identity.ready(), channel descriptions, invite/attachments bootstrap if onboarded |
register({ username, displayName }) |
_bootstrapAfterUser |
importIdentityBundle(bundle) |
Device pair restore |
onboarded, identity, storagePath, db, dbPath |
Session state |
listGuilds(), loadGuild(guildId), createGuild({ name, publicListing? }), joinInvite(code) |
Guild session |
Messaging & channels
| Method | Role |
|---|---|
selectChannel(channelId) |
Sets messages.setChannel, read state, voice context |
sendMessage, editMessage, deleteMessage |
Delegates to PearcordMessage + guild/DM gossip |
signalTyping, toggleReaction |
Gossip + local state |
createCategory, createForumPost, thread helpers |
PearcordGuild + permissions |
Invites & discovery
| Method | Role |
|---|---|
createInvite() |
PearcordInvite#create with channel snapshot |
joinInvite(code) |
Resolve + guild.joinByInvite + mesh; pearcord://explore?… → navigateDeepLink (v0.8.108) |
markOnboardingExploreShown() |
Clear first-guild Explore prompt; onboardingExploreShown pref (v0.8.108) |
setThreadsPanelFilter(filter) |
Filter chips; persists threadsPanelFilter in USER_PREFS (v0.8.108) |
formatDiscoveryExploreBaselineDeepLink() |
pearcord://explore?baseline&at share URL (v0.8.107) |
Voice & media
| Method | Role |
|---|---|
joinVoice, leaveVoice, setVoiceMute |
pearcord-voice + guild gossip |
setVoiceRosterChannelExpanded(channelId, expanded) |
USER_PREFS voiceRosterExpandedChannelIds (v0.8.110) |
setDiscoveryFilter({ query, sort, minMembers, tag }) |
Persists discoveryExploreFilter in USER_PREFS (v0.8.111) |
| Voice/s screen share ingest | VoiceMediaHub, ScreenShareHub wired in _openGuild |
Views & companion
| Property | Role |
|---|---|
mode |
'home', 'guild', or DM mode |
getViewState() / snapshot builders |
IPC state payloads for UI |
_readOnly, _companionMode |
Companion restrictions |
Hundreds of additional methods cover moderation, bots, search, audit export, forums, stage, automod, boosts, settings mesh, contacts, notifications — each delegating to the matching pearcord-* package.
P2P surface
Platform does not implement wire protocols; it orchestrates:
| Subsystem | Module | Topic / protocol |
|---|---|---|
| Guild gossip | pearcord-guild |
pearcord-gossip-v1 on guild.topic |
| DM mesh | pearcord-dm |
dmTopic / group DM topics |
| Contacts | pearcord-contacts |
contactsTopic |
| Settings / prefs | pearcord-settings |
userPrefsTopic |
| Discovery | pearcord-discovery |
Public listings mesh |
| Device pair | pearcord-device-sync |
deviceSyncTopic + DEVICE_PAIR_* RPC |
_wireGuild(guild) registers setMessageHandler and forwards guild events to platform.emit (message, member, guild-sync, voice, …).
Storage
| Path | Owner |
|---|---|
{storagePath}/db/ |
LocalDatabase — all @pearcord/* collections |
{storagePath}/identity/ |
pearcord-identity keypairs |
{storagePath}/dm-meta/ |
JsonStore for DM metadata |
| Feature dirs | attachments, discovery JSON, moderation, voice state, etc. |
storagePath from resolveStoragePath() (PEARCORD_STORAGE or ~/.config/pearcord).
Platform integration (core batch A wiring)
// Constructor (simplified)
this.db = new LocalDatabase(this.dbPath)
this.identity = new PearcordIdentity({ db, storagePath })
// ready() → invite, attachments, discovery when snap.user
// Guild open (_openGuild)
this.guild = new PearcordGuild({ ownerId, userId, db })
this._wireGuild(this.guild)
await this.guild.joinMesh()
this.messages = new PearcordMessage({ authorId: user.id, db })
| Dependency | Role in platform |
|---|---|
pearcord-db |
Shared this.db |
pearcord-identity |
this.identity, storage paths |
pearcord-guild |
Active server + mesh |
pearcord-message |
this.messages per channel |
pearcord-invite |
this.invite |
pearcord-shared |
Permissions, COLLECTIONS, deep links, id, now |
pearcord-channel and pearcord-sync are not dependencies.
UI / IPC
Primary consumer: apps/pearcord/index.js sidecar reading pear-pipe JSON.
| Direction | Examples |
|---|---|
| UI → platform | register, create-guild, join-invite, select-guild, select-channel, send-message, join-voice |
v0.8.115: loadGuild / _openGuild clears stale activeChannelId, then _selectGuildChannelWithUnread picks mention-weighted unread channels before falling back to the first text channel. setSearch records searchRecentByGuild in user prefs. View: searchRecentQueries.
v0.8.123: Equal unread/mention scores tie-break to lower channel.position. Smoke: test:guild-unread-position-tie.
v0.8.120: _selectGuildChannelWithUnread scores mentions × 1000 + unread; mention channel beats higher plain unread. Smoke: test:guild-mention-channel-pick.
v0.8.119: _selectGuildChannelWithUnread order: highest unread/mention score → lastChannelByGuild → first text. Smokes: test:guild-unread-over-last-channel, test:guild-unread-channel-jump.
v0.8.116: selectChannel persists savable channel types via _persistLastGuildChannel. Smoke: npm run test:guild-last-channel-prefs.
| platform → UI | state, message, channel, notification, session-ready, user |
See IPC.md for the full message table. UI should not import guild/message modules directly in production builds.
Related docs
- IPC.md — sidecar contract
- ARCHITECTURE.md — system diagram
- MODULES.md — all packages
- GETTING_STARTED.md
- ONBOARDING.md
- GUILD_SYNC.md
- PEARCORD_PARITY.md
- AUTOMATED_TESTING.md
- PLATFORM_ROADMAP.md
Tests
Most apps/pearcord/scripts/smoke-*.cjs scripts construct PearcordPlatform with a temp storagePath. Runner: npm run test:smoke / smoke-runner.cjs.
Representative targets:
- Guild/invite:
test:guild-sync,test:invite,test:storage-path - Mesh RPC:
smoke-voice-speaking-mesh.cjs,smoke-guild-sync.cjs - HyperDB:
test:hyperdb(db only, but platform uses same path layout)
Code example
const path = require('bare-path')
const os = require('bare-os')
const { PearcordPlatform } = require('pearcord-platform')
const storagePath = path.join(os.tmpdir(), `pearcord-demo-${Date.now()}`)
const platform = new PearcordPlatform({ storagePath })
await platform.ready()
await platform.register({ username: 'demo', displayName: 'Demo' })
const { guild } = await platform.createGuild({ name: 'Test Server' })
await platform.sendMessage('Hello from the facade')
const view = platform.getViewState?.() ?? { mode: platform.mode, guildId: guild.id }
console.log(view)
await platform.close?.()
Minimal sidecar-style usage:
platform.on('message', (msg) => { /* push to UI */ })
platform.on('session-ready', () => { /* enable guild list */ })
Repository
Part of Pearcord.
- Org:
pearcord - Clone:
git clone https://git.ssh.surf/pearcord/pearcord-platform.git - Install:
npm install git+https://git.ssh.surf/pearcord/pearcord-platform.git#main
Depends on the full Pearcord module workspace (see package.json); install from monorepo modules/ for local development.