move labeler cache to mmkv, make moderation config sync

The per-account labeler cache was the only async dependency in
configureModerationForAccount. MMKV-backed sync reads let session
setup apply labelers in the same tick. No AsyncStorage backfill: the
cache is rewritten on every preferences fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-31 19:01:05 +03:00
parent e79d8e8e52
commit 045f3965ec
5 changed files with 51 additions and 24 deletions
+2 -2
View File
@@ -21,7 +21,7 @@ import {
} from '#/state/queries/preferences/types' } from '#/state/queries/preferences/types'
import {createQueryKey} from '#/state/queries/util' import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {saveLabelers} from '#/state/session/agent-config' import {saveLabelers} from '#/state/session/moderation'
import {useAgeAssurance} from '#/ageAssurance' import {useAgeAssurance} from '#/ageAssurance'
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util' import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
@@ -53,7 +53,7 @@ export function usePreferencesQuery() {
const res = await agent.getPreferences() const res = await agent.getPreferences()
// save to local storage to ensure there are labels on initial requests // save to local storage to ensure there are labels on initial requests
void saveLabelers( saveLabelers(
agent.did, agent.did,
res.moderationPrefs.labelers.map(l => l.did), res.moderationPrefs.labelers.map(l => l.did),
) )
-12
View File
@@ -1,12 +0,0 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
const PREFIX = 'agent-labelers'
export async function saveLabelers(did: string, value: string[]) {
await AsyncStorage.setItem(`${PREFIX}:${did}`, JSON.stringify(value))
}
export async function readLabelers(did: string): Promise<string[] | undefined> {
const rawData = await AsyncStorage.getItem(`${PREFIX}:${did}`)
return rawData ? JSON.parse(rawData) : undefined
}
+6 -6
View File
@@ -64,7 +64,7 @@ export async function createAgentAndResume(
const gates = features.refresh({ const gates = features.refresh({
strategy: 'prefer-low-latency', strategy: 'prefer-low-latency',
}) })
const moderation = configureModerationForAccount(agent, storedAccount) configureModerationForAccount(agent, storedAccount)
const prevSession: AtpSessionData = sessionAccountToSession(storedAccount) const prevSession: AtpSessionData = sessionAccountToSession(storedAccount)
if (isSessionExpired(storedAccount)) { if (isSessionExpired(storedAccount)) {
await networkRetry(1, () => agent.resumeSession(prevSession)) await networkRetry(1, () => agent.resumeSession(prevSession))
@@ -78,7 +78,7 @@ export async function createAgentAndResume(
agent.configureProxy(BLUESKY_PROXY_HEADER.get()) agent.configureProxy(BLUESKY_PROXY_HEADER.get())
return agent.prepare({ return agent.prepare({
resolvers: [gates, moderation, aa], resolvers: [gates, aa],
onSessionChange, onSessionChange,
}) })
} }
@@ -111,13 +111,13 @@ export async function createAgentAndLogin(
const account = agentToSessionAccountOrThrow(agent) const account = agentToSessionAccountOrThrow(agent)
const gates = features.refresh({strategy: 'prefer-fresh-gates'}) const gates = features.refresh({strategy: 'prefer-fresh-gates'})
const moderation = configureModerationForAccount(agent, account) configureModerationForAccount(agent, account)
const aa = prefetchAgeAssuranceServerData({agent}) const aa = prefetchAgeAssuranceServerData({agent})
agent.configureProxy(BLUESKY_PROXY_HEADER.get()) agent.configureProxy(BLUESKY_PROXY_HEADER.get())
return agent.prepare({ return agent.prepare({
resolvers: [gates, moderation, aa], resolvers: [gates, aa],
onSessionChange, onSessionChange,
}) })
} }
@@ -159,7 +159,7 @@ export async function createAgentAndCreateAccount(
}) })
const account = agentToSessionAccountOrThrow(agent) const account = agentToSessionAccountOrThrow(agent)
const gates = features.refresh({strategy: 'prefer-fresh-gates'}) const gates = features.refresh({strategy: 'prefer-fresh-gates'})
const moderation = configureModerationForAccount(agent, account) configureModerationForAccount(agent, account)
const createdAt = new Date().toISOString() const createdAt = new Date().toISOString()
const birthdate = birthDate.toISOString() const birthdate = birthDate.toISOString()
@@ -277,7 +277,7 @@ export async function createAgentAndCreateAccount(
agent.configureProxy(BLUESKY_PROXY_HEADER.get()) agent.configureProxy(BLUESKY_PROXY_HEADER.get())
return agent.prepare({ return agent.prepare({
resolvers: [gates, moderation, aa], resolvers: [gates, aa],
onSessionChange, onSessionChange,
}) })
} }
+32 -4
View File
@@ -1,10 +1,31 @@
import {AtpAgent, BSKY_LABELER_DID} from '@atproto/api' import {AtpAgent, BSKY_LABELER_DID} from '@atproto/api'
import {IS_TEST_USER} from '#/lib/constants' import {IS_TEST_USER} from '#/lib/constants'
import {account as accountStorage} from '#/storage'
import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities' import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
import {readLabelers} from './agent-config'
import {type SessionAccount} from './types' import {type SessionAccount} from './types'
/**
* Cache an account's subscribed labeler DIDs. Called on every preferences
* fetch, so the cache is eventually consistent with the server.
*/
export function saveLabelers(did: string, value: string[]) {
accountStorage.set([did, 'labelers'], value)
}
/**
* Read the cached labeler DIDs for an account, or `undefined` if none have
* been cached yet (first session on this device) or the entry is unreadable.
*/
export function readLabelers(did: string): string[] | undefined {
try {
return accountStorage.get([did, 'labelers'])
} catch {
/* a corrupt entry fails JSON.parse inside Storage.get; treat as no cache */
return undefined
}
}
export function configureModerationForGuest() { export function configureModerationForGuest() {
// This global mutation is *only* OK because this code is only relevant for testing. // This global mutation is *only* OK because this code is only relevant for testing.
// Don't add any other global behavior here! // Don't add any other global behavior here!
@@ -12,7 +33,12 @@ export function configureModerationForGuest() {
configureAdditionalModerationAuthorities() configureAdditionalModerationAuthorities()
} }
export async function configureModerationForAccount( /**
* Configure global app labelers and the account's cached labeler
* subscriptions. Fully synchronous so session setup can apply labeler headers
* in the same tick, before any request goes out.
*/
export function configureModerationForAccount(
agent: AtpAgent, agent: AtpAgent,
account: SessionAccount, account: SessionAccount,
) { ) {
@@ -20,11 +46,12 @@ export async function configureModerationForAccount(
// Don't add any other global behavior here! // Don't add any other global behavior here!
switchToBskyAppLabeler() switchToBskyAppLabeler()
if (IS_TEST_USER(account.handle)) { if (IS_TEST_USER(account.handle)) {
await trySwitchToTestAppLabeler(agent) // Test accounts may briefly use the production authority while this resolves.
void trySwitchToTestAppLabeler(agent)
} }
// The code below is actually relevant to production (and isn't global). // The code below is actually relevant to production (and isn't global).
const labelerDids = await readLabelers(account.did).catch(_ => {}) const labelerDids = readLabelers(account.did)
if (labelerDids) { if (labelerDids) {
agent.configureLabelersHeader( agent.configureLabelersHeader(
labelerDids.filter(did => did !== BSKY_LABELER_DID), labelerDids.filter(did => did !== BSKY_LABELER_DID),
@@ -41,6 +68,7 @@ function switchToBskyAppLabeler() {
AtpAgent.configure({appLabelers: [BSKY_LABELER_DID]}) AtpAgent.configure({appLabelers: [BSKY_LABELER_DID]})
} }
/** Resolve and install the test environment's moderation authority. */
async function trySwitchToTestAppLabeler(agent: AtpAgent) { async function trySwitchToTestAppLabeler(agent: AtpAgent) {
const did = ( const did = (
await agent await agent
+11
View File
@@ -108,4 +108,15 @@ export type Account = {
* account after a switch, until that account's preferences loaded. * account after a switch, until that account's preferences loaded.
*/ */
isBetaUser?: boolean isBetaUser?: boolean
/**
* The account's subscribed labeler DIDs, cached from preferences so the
* `atproto-accept-labelers` header can be configured synchronously at
* session start, before preferences load. Eventually consistent: rewritten
* on every preferences fetch (see `saveLabelers` in
* `#/state/session/moderation`). Until the first fetch lands there is
* simply no cache entry and initial requests go out without per-account
* labeler headers.
*/
labelers?: string[]
} }