diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 41adfc9a04..a5803a9c7b 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -21,7 +21,7 @@ import { } from '#/state/queries/preferences/types' import {createQueryKey} from '#/state/queries/util' import {useAgent} from '#/state/session' -import {saveLabelers} from '#/state/session/agent-config' +import {saveLabelers} from '#/state/session/moderation' import {useAgeAssurance} from '#/ageAssurance' import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util' import {useAnalytics} from '#/analytics' @@ -53,7 +53,7 @@ export function usePreferencesQuery() { const res = await agent.getPreferences() // save to local storage to ensure there are labels on initial requests - void saveLabelers( + saveLabelers( agent.did, res.moderationPrefs.labelers.map(l => l.did), ) diff --git a/src/state/session/agent-config.ts b/src/state/session/agent-config.ts deleted file mode 100644 index 3ee2718a39..0000000000 --- a/src/state/session/agent-config.ts +++ /dev/null @@ -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 { - const rawData = await AsyncStorage.getItem(`${PREFIX}:${did}`) - return rawData ? JSON.parse(rawData) : undefined -} diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 78e9f79610..0d92199bdd 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -64,7 +64,7 @@ export async function createAgentAndResume( const gates = features.refresh({ strategy: 'prefer-low-latency', }) - const moderation = configureModerationForAccount(agent, storedAccount) + configureModerationForAccount(agent, storedAccount) const prevSession: AtpSessionData = sessionAccountToSession(storedAccount) if (isSessionExpired(storedAccount)) { await networkRetry(1, () => agent.resumeSession(prevSession)) @@ -78,7 +78,7 @@ export async function createAgentAndResume( agent.configureProxy(BLUESKY_PROXY_HEADER.get()) return agent.prepare({ - resolvers: [gates, moderation, aa], + resolvers: [gates, aa], onSessionChange, }) } @@ -111,13 +111,13 @@ export async function createAgentAndLogin( const account = agentToSessionAccountOrThrow(agent) const gates = features.refresh({strategy: 'prefer-fresh-gates'}) - const moderation = configureModerationForAccount(agent, account) + configureModerationForAccount(agent, account) const aa = prefetchAgeAssuranceServerData({agent}) agent.configureProxy(BLUESKY_PROXY_HEADER.get()) return agent.prepare({ - resolvers: [gates, moderation, aa], + resolvers: [gates, aa], onSessionChange, }) } @@ -159,7 +159,7 @@ export async function createAgentAndCreateAccount( }) const account = agentToSessionAccountOrThrow(agent) const gates = features.refresh({strategy: 'prefer-fresh-gates'}) - const moderation = configureModerationForAccount(agent, account) + configureModerationForAccount(agent, account) const createdAt = new Date().toISOString() const birthdate = birthDate.toISOString() @@ -277,7 +277,7 @@ export async function createAgentAndCreateAccount( agent.configureProxy(BLUESKY_PROXY_HEADER.get()) return agent.prepare({ - resolvers: [gates, moderation, aa], + resolvers: [gates, aa], onSessionChange, }) } diff --git a/src/state/session/moderation.ts b/src/state/session/moderation.ts index 8fc234d732..2354fab4fa 100644 --- a/src/state/session/moderation.ts +++ b/src/state/session/moderation.ts @@ -1,10 +1,31 @@ import {AtpAgent, BSKY_LABELER_DID} from '@atproto/api' import {IS_TEST_USER} from '#/lib/constants' +import {account as accountStorage} from '#/storage' import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities' -import {readLabelers} from './agent-config' 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() { // This global mutation is *only* OK because this code is only relevant for testing. // Don't add any other global behavior here! @@ -12,7 +33,12 @@ export function configureModerationForGuest() { 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, account: SessionAccount, ) { @@ -20,11 +46,12 @@ export async function configureModerationForAccount( // Don't add any other global behavior here! switchToBskyAppLabeler() 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). - const labelerDids = await readLabelers(account.did).catch(_ => {}) + const labelerDids = readLabelers(account.did) if (labelerDids) { agent.configureLabelersHeader( labelerDids.filter(did => did !== BSKY_LABELER_DID), @@ -41,6 +68,7 @@ function switchToBskyAppLabeler() { AtpAgent.configure({appLabelers: [BSKY_LABELER_DID]}) } +/** Resolve and install the test environment's moderation authority. */ async function trySwitchToTestAppLabeler(agent: AtpAgent) { const did = ( await agent diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 2f4424792d..53b4ffb022 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -108,4 +108,15 @@ export type Account = { * account after a switch, until that account's preferences loaded. */ 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[] }