phase 4 foundations: vendor missing lexicons, refreshSession api, toLex no-op purge, de-agent link-meta

This commit is contained in:
Samuel Newman
2026-07-17 11:55:07 +03:00
parent d13d1ca604
commit d052d14f2b
80 changed files with 649 additions and 450 deletions
+4 -9
View File
@@ -2,7 +2,6 @@ import {type Client} from '@atproto/lex-client'
import {DEMO_FEED} from '#/lib/demo'
import {type app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {type FeedAPI, type FeedAPIResponse} from './types'
export class DemoFeedAPI implements FeedAPI {
@@ -12,17 +11,13 @@ export class DemoFeedAPI implements FeedAPI {
this.client = client
}
// eslint-disable-next-line @typescript-eslint/require-await
async peekLatest(): Promise<app.bsky.feed.defs.FeedViewPost> {
/*
* TODO(phase4): drop this cast once `#/lib/demo` (DEMO_FEED) sources its
* feed items from `#/lexicons`. Its records are still typed against the old
* `@atproto/api` FeedViewPost, which does not assign to the branded lexicon
* type this method must return.
*/
return toLex(DEMO_FEED.feed[0])
return DEMO_FEED.feed[0]
}
// eslint-disable-next-line @typescript-eslint/require-await
async fetch(): Promise<FeedAPIResponse> {
return toLex(DEMO_FEED)
return DEMO_FEED
}
}
+1 -8
View File
@@ -8,7 +8,6 @@ import {feedUriToHref} from '#/lib/strings/url-helpers'
import {getContentLanguages} from '#/state/preferences/languages'
import {type FeedParams} from '#/state/queries/post-feed'
import {app} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {FeedTuner} from '../feed-manip'
import {type FeedTunerFn} from '../feed-manip'
import {
@@ -264,13 +263,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
return {
success: true,
cursor: res.cursor,
/*
* TODO(phase4): drop the toLex once `#/lib/api/feed-manip` (FeedTuner)
* flips its FeedViewPost source from `@atproto/api` to `#/lexicons`. Its
* `_feedPost` is still the old-world view type, which does not assign to
* the branded lexicon type this page carries.
*/
feed: slices.map(slice => toLex(slice._feedPost)),
feed: slices.map(slice => slice._feedPost),
}
}
}
+10 -35
View File
@@ -91,16 +91,7 @@ export async function post(
for (let i = 0; i < thread.posts.length; i++) {
const draft = thread.posts[i]
/*
* Not awaited to avoid waterfalls. `draft.richtext` is still an
* `@atproto/api` RichText (composer state is migrated by Task 7); the SDK
* RichText is structurally the same transplant, so bridge it here.
* TODO(phase4): drop toLex once composer state migrates to SDK RichText.
*/
const rtPromise = resolveRT(
resolveClients.appview,
bsky.toLex<RichText>(draft.richtext),
)
const rtPromise = resolveRT(resolveClients.appview, draft.richtext)
const embedPromise = resolveEmbed(
clients,
queryClient,
@@ -138,17 +129,11 @@ export async function post(
langs,
labels,
}
/*
* `value` is typed `LexMap` (a loose index-signature map). A generated
* record type is a valid LexMap at runtime but a strict interface is not
* assignable to an index-signature type, so widen with `toLex` at this
* boundary. Not a brand cast - the record is already fully typed.
*/
writes.push({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.post',
rkey: rkey,
value: bsky.toLex(record),
value: record,
})
if (i === 0 && thread.threadgate.some(tg => tg.type !== 'everybody')) {
@@ -156,14 +141,10 @@ export async function post(
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.threadgate',
rkey: rkey,
value: bsky.toLex(
createThreadgateRecord({
post: uri,
allow: threadgateAllowUISettingToAllowRecordValue(
thread.threadgate,
),
}),
),
value: createThreadgateRecord({
post: uri,
allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate),
}),
})
}
@@ -175,12 +156,12 @@ export async function post(
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.postgate',
rkey: rkey,
value: bsky.toLex({
value: {
...thread.postgate,
$type: 'app.bsky.feed.postgate',
createdAt: now.toISOString(),
post: uri,
}),
},
})
}
@@ -426,7 +407,7 @@ async function resolveMedia(
* structural shape matches the lexicon blob field; the CID hasher handles
* both class instances and plain lex blobs (see computeCid).
*/
video: bsky.toLex(videoDraft.pendingPublish.blobRef),
video: videoDraft.pendingPublish.blobRef,
alt: videoDraft.altText || undefined,
captions: captions.length === 0 ? undefined : captions,
aspectRatio,
@@ -475,13 +456,7 @@ async function resolveMedia(
title: resolvedLink.title,
description: resolvedLink.description,
thumb: blob,
/*
* associatedRefs comes from getLinkMeta, still typed with the old
* `@atproto/api` StrongRef (link-meta.ts is out of scope). The shape
* is identical; bridge with toLex.
* TODO(phase4): drop toLex once link-meta migrates.
*/
associatedRefs: bsky.toLex(resolvedLink.associatedRefs),
associatedRefs: resolvedLink.associatedRefs,
},
}
}
+3 -15
View File
@@ -23,7 +23,6 @@ import {
import {type ComposerImage} from '#/state/gallery'
import {createComposerImage} from '#/state/gallery'
import {type ChatInvitePreview} from '#/state/queries/join-links'
import {type SessionAgent} from '#/state/session'
import {type Gif} from '#/features/gifPicker/types'
import {app, chat, com} from '#/lexicons'
import {createGIFDescription} from '../gif-alt-text'
@@ -31,13 +30,10 @@ import {createGIFDescription} from '../gif-alt-text'
/**
* Clients the link resolver needs. `appview` serves the `app.bsky.*` feed/graph
* reads plus handle resolution; `chat` serves the group join-link previews.
* `agent` is still threaded through to {@link getLinkMeta}, which has not yet
* migrated off the bridge - it only reads the (unused) service URL from it.
*/
export type ResolveClients = {
appview: Client
chat: Client
agent: SessionAgent
}
type ResolvedExternalLink = {
@@ -195,7 +191,7 @@ export async function resolveLink(
view: res.starterPack,
}
}
return resolveExternal(clients, uri)
return resolveExternal(uri)
// Forked from useGetPost. TODO: move into RQ.
async function getPost({uri}: {uri: string}) {
@@ -267,16 +263,8 @@ function getFileSlug(url: string | undefined): string | undefined {
return dotIndex > 0 ? filename.slice(0, dotIndex) : undefined
}
async function resolveExternal(
clients: ResolveClients,
uri: string,
): Promise<ResolvedExternalLink> {
/*
* getLinkMeta still takes the bridge agent (not yet migrated); it only reads
* a service URL from it that LINK_META_PROXY ignores. Keep threading the
* agent here until link-meta migrates.
*/
const result = await getLinkMeta(clients.agent, uri)
async function resolveExternal(uri: string): Promise<ResolvedExternalLink> {
const result = await getLinkMeta(uri)
return {
type: 'external',
uri: result.url,
+1 -5
View File
@@ -1,7 +1,6 @@
import {LINK_META_PROXY} from '#/lib/constants'
import {getGiphyMetaUri} from '#/lib/strings/embed-player'
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
import {type SessionAgent} from '#/state/session'
import {type app} from '#/lexicons'
import {isBskyAppUrl} from '../strings/url-helpers'
@@ -31,7 +30,6 @@ export interface LinkMeta {
}
export async function getLinkMeta(
agent: SessionAgent,
url: string,
timeout = 15e3,
): Promise<LinkMeta> {
@@ -80,9 +78,7 @@ export async function getLinkMeta(
try {
const response = await fetch(
`${LINK_META_PROXY(agent.serviceUrl.toString() || '')}${encodeURIComponent(
url,
)}`,
`${LINK_META_PROXY('')}${encodeURIComponent(url)}`,
{signal: controller.signal},
)
+2 -15
View File
@@ -12,7 +12,6 @@ import {
} from '#/lib/moderation/useGlobalLabelStrings'
import {useLabelDefinitions} from '#/state/preferences'
import {type app, type com} from '#/lexicons'
import {toLex} from '#/types/bsky'
export interface LabelInfo {
label: com.atproto.label.defs.Label
@@ -23,26 +22,14 @@ export interface LabelInfo {
export function useLabelInfo(label: com.atproto.label.defs.Label): LabelInfo {
const {i18n} = useLingui()
/*
* TODO(phase4): drop the `toLex` casts once `useLabelDefinitions`
* (state/preferences/label-defs) emits `#/lexicons` / `@bsky.app/sdk`
* moderation types. It still returns old `@atproto/api`
* `InterpretedLabelValueDefinition` / `LabelerViewDetailed`, which are
* structurally identical to the SDK/lexicon ones modulo branded strings.
*/
const {labelDefs, labelers} = useLabelDefinitions()
const def = getDefinition(
toLex<Record<string, InterpretedLabelValueDefinition[]>>(labelDefs),
label,
)
const def = getDefinition(labelDefs, label)
const globalLabelStrings = useGlobalLabelStrings()
return {
label,
def,
strings: getLabelStrings(i18n.locale, globalLabelStrings, def),
labeler: toLex<app.bsky.labeler.defs.LabelerViewDetailed[]>(labelers).find(
labeler => label.src === labeler.creator.did,
),
labeler: labelers.find(labeler => label.src === labeler.creator.did),
}
}
@@ -13,8 +13,6 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {type AppModerationCause} from '#/components/Pills'
import {type com} from '#/lexicons'
import {toLex} from '#/types/bsky'
import {useGlobalLabelStrings} from './useGlobalLabelStrings'
import {getDefinition, getLabelStrings} from './useLabelInfo'
@@ -129,18 +127,7 @@ export function useModerationCauseDescription(
}
}
if (cause.type === 'label') {
/*
* TODO(phase4): drop `toLex` once `useLabelDefinitions` and the label
* cause type share the `#/lexicons` label shape. `labelDefs` still comes
* from the old `@atproto/api`-typed producer and `cause.label` is the SDK
* label; both are structurally identical to the `#/lexicons` label.
*/
const def =
cause.labelDef ||
getDefinition(
toLex<Parameters<typeof getDefinition>[0]>(labelDefs),
toLex<com.atproto.label.defs.Label>(cause.label),
)
const def = cause.labelDef || getDefinition(labelDefs, cause.label)
const strings = getLabelStrings(i18n.locale, globalLabelStrings, def)
const labeler = labelers.find(l => l.creator.did === cause.label.src)
let source = labeler