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:
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user