collapse the dual-world type layer and delete the widening shims
The types/bsky post/profile/starterPack unions drop their @atproto/api arms, and dangerousIsType/validate go with the old-world guards they wrapped. The moderation subjects.ts widening shim and rich-text-helpers' asSdkFacets both existed only to bridge branded and unbranded views, so their 55 and 14 callers now go straight to @bsky.app/sdk/moderation and the raw facets. Boundary fallout: lexicon token defs are camelCase schema objects needing .value, and the branded string slots that the widening used to absorb are now cast or branded at their producers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,12 @@ import {type FeedAPI, type FeedAPIResponse} from './types'
|
||||
// we use this fallback marker post to drive this instead. see Feed.tsx
|
||||
// for the usage.
|
||||
// -prf
|
||||
export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = {
|
||||
/*
|
||||
* A synthetic marker, not a real view: its `uri`/`did`/`indexedAt` are
|
||||
* deliberately not well-formed, so the literal is asserted rather than branded.
|
||||
* Only `post.uri` is ever read (see Feed.tsx).
|
||||
*/
|
||||
export const FALLBACK_MARKER_POST = {
|
||||
post: {
|
||||
uri: 'fallback-marker-post',
|
||||
cid: 'fake',
|
||||
@@ -26,7 +31,7 @@ export const FALLBACK_MARKER_POST: app.bsky.feed.defs.FeedViewPost = {
|
||||
},
|
||||
indexedAt: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
} as unknown as app.bsky.feed.defs.FeedViewPost
|
||||
|
||||
export class HomeFeedAPI implements FeedAPI {
|
||||
client: Client
|
||||
|
||||
+7
-5
@@ -1,11 +1,13 @@
|
||||
import {toDatetimeString} from '@atproto/syntax'
|
||||
import {subDays, subMinutes} from 'date-fns'
|
||||
import {app} from '#/lexicons'
|
||||
|
||||
import {type app} from '#/lexicons'
|
||||
|
||||
const DID = `did:plc:z72i7hdynmk6r22z27h6tvur`
|
||||
const NOW = new Date()
|
||||
const POST_1_DATE = subMinutes(NOW, 2).toISOString()
|
||||
const POST_2_DATE = subMinutes(NOW, 4).toISOString()
|
||||
const POST_3_DATE = subMinutes(NOW, 5).toISOString()
|
||||
const POST_1_DATE = toDatetimeString(subMinutes(NOW, 2))
|
||||
const POST_2_DATE = toDatetimeString(subMinutes(NOW, 4))
|
||||
const POST_3_DATE = toDatetimeString(subMinutes(NOW, 5))
|
||||
|
||||
export const DEMO_FEED = {
|
||||
feed: [
|
||||
@@ -31,7 +33,7 @@ export const DEMO_FEED = {
|
||||
issuer: DID,
|
||||
uri: `at://${DID}/app.bsky.graph.verification/post1`,
|
||||
isValid: true,
|
||||
createdAt: subDays(NOW, 11).toISOString(),
|
||||
createdAt: toDatetimeString(subDays(NOW, 11)),
|
||||
},
|
||||
],
|
||||
verifiedStatus: 'valid',
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import {
|
||||
hasMutedWord as sdkHasMutedWord,
|
||||
moderateFeedGenerator as sdkModerateFeedGenerator,
|
||||
moderateNotification as sdkModerateNotification,
|
||||
moderatePost as sdkModeratePost,
|
||||
moderateProfile as sdkModerateProfile,
|
||||
moderateStatus as sdkModerateStatus,
|
||||
moderateUserList as sdkModerateUserList,
|
||||
type ModerationDecision,
|
||||
type ModerationOpts,
|
||||
} from '@bsky.app/sdk/moderation'
|
||||
|
||||
import {app, chat} from '#/lexicons'
|
||||
|
||||
/*
|
||||
* TRANSITIONAL. The moderation implementation now comes from
|
||||
* `@bsky.app/sdk/moderation`, whose subject types are the generated
|
||||
* `#/lexicons` views - so their `did`/`uri`/`cid` fields are branded
|
||||
* (`DidString`, `AtUriString`). Many read paths still emit the identically
|
||||
* shaped `@atproto/api` views, whose same fields are plain `string`.
|
||||
*
|
||||
* A plain `string` is not assignable to a branded template-literal type, so
|
||||
* every `moderate*` call taking an unmigrated view fails to typecheck even
|
||||
* though the value is byte-identical - the runtime only ever reads `.did`,
|
||||
* `.labels` and `.viewer`, none of which the brand affects.
|
||||
*
|
||||
* These wrappers widen each subject parameter to accept a view from either
|
||||
* world and drop the brand on the way in. Delete this module once every
|
||||
* producer emits `#/lexicons` views (the `@atproto/api` removal pass) and point
|
||||
* callers back at `@bsky.app/sdk/moderation` directly.
|
||||
*/
|
||||
|
||||
type AnyProfileSubject =
|
||||
| app.bsky.actor.defs.ProfileViewBasic
|
||||
| app.bsky.actor.defs.ProfileView
|
||||
| app.bsky.actor.defs.ProfileViewDetailed
|
||||
| chat.bsky.actor.defs.ProfileViewBasic
|
||||
| app.bsky.actor.defs.ProfileViewBasic
|
||||
| app.bsky.actor.defs.ProfileView
|
||||
| app.bsky.actor.defs.ProfileViewDetailed
|
||||
| chat.bsky.actor.defs.ProfileViewBasic
|
||||
|
||||
type AnyPostSubject = app.bsky.feed.defs.PostView | app.bsky.feed.defs.PostView
|
||||
|
||||
type AnyUserListSubject =
|
||||
| app.bsky.graph.defs.ListViewBasic
|
||||
| app.bsky.graph.defs.ListView
|
||||
| app.bsky.graph.defs.ListViewBasic
|
||||
| app.bsky.graph.defs.ListView
|
||||
|
||||
type AnyFeedGeneratorSubject =
|
||||
| app.bsky.feed.defs.GeneratorView
|
||||
| app.bsky.feed.defs.GeneratorView
|
||||
|
||||
type AnyNotificationSubject =
|
||||
| app.bsky.notification.listNotifications.Notification
|
||||
| app.bsky.notification.listNotifications.Notification
|
||||
|
||||
export function moderateProfile(
|
||||
subject: AnyProfileSubject,
|
||||
opts: ModerationOpts,
|
||||
): ModerationDecision {
|
||||
return sdkModerateProfile(subject as app.bsky.actor.defs.ProfileView, opts)
|
||||
}
|
||||
|
||||
export function moderateStatus(
|
||||
subject: AnyProfileSubject,
|
||||
opts: ModerationOpts,
|
||||
): ModerationDecision {
|
||||
return sdkModerateStatus(subject as app.bsky.actor.defs.ProfileView, opts)
|
||||
}
|
||||
|
||||
export function moderatePost(
|
||||
subject: AnyPostSubject,
|
||||
opts: ModerationOpts,
|
||||
): ModerationDecision {
|
||||
return sdkModeratePost(subject as app.bsky.feed.defs.PostView, opts)
|
||||
}
|
||||
|
||||
export function moderateUserList(
|
||||
subject: AnyUserListSubject,
|
||||
opts: ModerationOpts,
|
||||
): ModerationDecision {
|
||||
return sdkModerateUserList(subject as app.bsky.graph.defs.ListView, opts)
|
||||
}
|
||||
|
||||
export function moderateFeedGenerator(
|
||||
subject: AnyFeedGeneratorSubject,
|
||||
opts: ModerationOpts,
|
||||
): ModerationDecision {
|
||||
return sdkModerateFeedGenerator(
|
||||
subject as app.bsky.feed.defs.GeneratorView,
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
export function moderateNotification(
|
||||
subject: AnyNotificationSubject,
|
||||
opts: ModerationOpts,
|
||||
): ModerationDecision {
|
||||
return sdkModerateNotification(
|
||||
subject as app.bsky.notification.listNotifications.Notification,
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Widens `facets`/`actor` for the same reason the `moderate*` wrappers widen
|
||||
* their subjects: mute-word matching reads only `text`/`features`/`langs`, none
|
||||
* of which the brand affects.
|
||||
*/
|
||||
export function hasMutedWord(params: {
|
||||
mutedWords: app.bsky.actor.defs.MutedWord[]
|
||||
text: string
|
||||
facets?: app.bsky.richtext.facet.Main[] | app.bsky.richtext.facet.Main[]
|
||||
outlineTags?: string[]
|
||||
languages?: string[]
|
||||
actor?: AnyProfileSubject
|
||||
}): boolean {
|
||||
return sdkHasMutedWord({
|
||||
...params,
|
||||
facets: params.facets as app.bsky.richtext.facet.Main[] | undefined,
|
||||
actor: params.actor as app.bsky.actor.defs.ProfileView | undefined,
|
||||
})
|
||||
}
|
||||
@@ -30,26 +30,3 @@ export function richTextToString(rt: RichText, loose: boolean): string {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Widens facets typed by the legacy `@atproto/api` codegen into the shape the
|
||||
* SDK's `RichText` accepts.
|
||||
*
|
||||
* The two are the same lexicon and identical at runtime; they differ only in
|
||||
* that the SDK brands `did`/`uri` as template literal types, which makes the
|
||||
* legacy `string` versions unassignable. Call this where facets read off an
|
||||
* `@atproto/api` view type are handed to `new RichText(...)`.
|
||||
*
|
||||
* Transitional: it goes away once the view types come from the SDK too.
|
||||
*/
|
||||
export function asSdkFacets(
|
||||
facets: {index: {byteStart: number; byteEnd: number}; features: unknown[]}[],
|
||||
): app.bsky.richtext.facet.Main[]
|
||||
export function asSdkFacets(
|
||||
facets:
|
||||
| {index: {byteStart: number; byteEnd: number}; features: unknown[]}[]
|
||||
| undefined,
|
||||
): app.bsky.richtext.facet.Main[] | undefined
|
||||
export function asSdkFacets(facets: unknown) {
|
||||
return facets as app.bsky.richtext.facet.Main[] | undefined
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
getMain,
|
||||
type InferMethodError,
|
||||
LexError,
|
||||
type Main,
|
||||
type Procedure,
|
||||
type Query,
|
||||
@@ -27,6 +28,19 @@ export function getErrorStatus(e: unknown): number | undefined {
|
||||
return e instanceof XrpcResponseError ? e.status : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* The lexicon error code (`err.error`), or undefined when `e` carries none.
|
||||
* Gated on `LexError` (the base of every `XrpcError`) rather than `XrpcError`,
|
||||
* so the non-XRPC lex errors are covered too.
|
||||
*
|
||||
* Prefer {@link matchXrpcError} where the method that threw is known: it
|
||||
* constrains the compared name to that method's declared errors. Use this only
|
||||
* where the source method is genuinely ambiguous.
|
||||
*/
|
||||
export function getErrorName(e: unknown): string | undefined {
|
||||
return e instanceof LexError ? e.error : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Same nsid means `e` was thrown for this method schema, so `e` can be
|
||||
* treated as an `XrpcResponseError<M>` - which is what lets the SDK's
|
||||
|
||||
Reference in New Issue
Block a user