A beautiful Tangled client with Catppuccin colors. snarled.at
catppuccin atproto tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

lib/tangled/repo: consolidate types and validators

+145 -94
+15
src/lib/tangled/repo/getIssue.ts
··· 1 + import { ok } from '@atcute/client' 2 + import type { ResourceUri } from '@atcute/lexicons' 3 + import { rpc } from '../client' 4 + import type { IssueRecord } from './types' 5 + import { validateIssueRecord } from './validators' 6 + 7 + export async function getIssue(issue: ResourceUri): Promise<IssueRecord> { 8 + const response = await ok( 9 + rpc.get('sh.tangled.repo.getIssue', { 10 + params: { issue }, 11 + }), 12 + ) 13 + 14 + return validateIssueRecord(response) 15 + }
+2 -1
src/lib/tangled/repo/getRepo.ts
··· 1 1 import { ok } from '@atcute/client' 2 2 import type { ResourceUri } from '@atcute/lexicons' 3 3 import { rpc } from '../client' 4 - import { validateRepo, type Repo } from './types' 4 + import type { Repo } from './types' 5 + import { validateRepo } from './validators' 5 6 6 7 export async function getRepo(atUri: ResourceUri): Promise<Repo> { 7 8 const repo = await ok(
+2 -1
src/lib/tangled/repo/getRepoByRepoDid.ts
··· 1 1 import type { Did } from '@atcute/lexicons' 2 2 import { rpc } from '../client' 3 - import { validateRepo, type Repo } from './types' 3 + import type { Repo } from './types' 4 + import { validateRepo } from './validators' 4 5 5 6 export async function getRepoByRepoDid(did: Did): Promise<Repo | null> { 6 7 const response = await rpc.get('sh.tangled.repo.getRepoByRepoDid', {
+15 -4
src/lib/tangled/repo/index.ts
··· 1 1 export { getDefaultBranch } from './getDefaultBranch' 2 + export { getIssue } from './getIssue' 2 3 export { isDirectoryMode } from './isDirectoryMode' 3 4 export { getRecentCommits } from './getRecentCommits' 4 5 export type { RecentCommitsOptions, RepoCommit } from './getRecentCommits' ··· 8 9 export type { RepoLogOptions } from './getRepoLog' 9 10 export { getRepoTree } from './getRepoTree' 10 11 export { listIssues } from './listIssues' 11 - export type { Issue, IssueList, ListIssuesOptions } from './listIssues' 12 + export type { ListIssuesOptions } from './listIssues' 12 13 export { listPulls } from './listPulls' 13 - export type { ListPullsOptions, Pull, PullList } from './listPulls' 14 + export type { ListPullsOptions } from './listPulls' 14 15 export { listRepos } from './listRepos' 15 - export { getRepoName, getRepoRkey } from './types' 16 - export type { Repo, RepoList } from './types' 16 + export { getRepoName, getRepoRkey } from './repoIdentity' 17 + export type { 18 + Issue, 19 + IssueList, 20 + IssueRecord, 21 + Pull, 22 + PullList, 23 + PullRecord, 24 + Repo, 25 + RepoList, 26 + TangledRecord, 27 + } from './types'
+3 -22
src/lib/tangled/repo/listIssues.ts
··· 1 1 import { ok } from '@atcute/client' 2 2 import type { Did } from '@atcute/lexicons' 3 3 import { safeParse } from '@atcute/lexicons' 4 - import type { Main as TangledIssue } from '@atcute/tangled/types/repo/issue' 5 - import { mainSchema as issueSchema } from '@atcute/tangled/types/repo/issue' 6 - import type { $output as IssueListResponse } from '@atcute/tangled/types/repo/listIssues' 7 4 import { mainSchema as issueListSchema } from '@atcute/tangled/types/repo/listIssues' 8 5 import { rpc } from '../client' 9 6 import { removeNullCursor } from '../utils' 10 - 11 - export type Issue = Omit<IssueListResponse['items'][number], 'value'> & { 12 - value: TangledIssue 13 - } 14 - 15 - export type IssueList = { 16 - items: Issue[] 17 - cursor?: string 18 - } 7 + import type { IssueList } from './types' 8 + import { validateIssue } from './validators' 19 9 20 10 export type ListIssuesOptions = { 21 11 cursor?: string ··· 45 35 throw new Error(`Bobbin returned an invalid issue list: ${listValidation.message}`) 46 36 } 47 37 48 - const items = listValidation.value.items.map((item, index) => { 49 - const valueValidation = safeParse(issueSchema, item.value) 50 - if (!valueValidation.ok) { 51 - throw new Error( 52 - `Bobbin returned an invalid issue record at index ${index}: ${valueValidation.message}`, 53 - ) 54 - } 55 - 56 - return { ...item, value: valueValidation.value } 57 - }) 38 + const items = listValidation.value.items.map(validateIssue) 58 39 59 40 return { items, cursor: listValidation.value.cursor } 60 41 }
+3 -22
src/lib/tangled/repo/listPulls.ts
··· 1 1 import { ok } from '@atcute/client' 2 2 import type { Did } from '@atcute/lexicons' 3 3 import { safeParse } from '@atcute/lexicons' 4 - import type { Main as TangledPull } from '@atcute/tangled/types/repo/pull' 5 - import { mainSchema as pullSchema } from '@atcute/tangled/types/repo/pull' 6 - import type { $output as PullListResponse } from '@atcute/tangled/types/repo/listPulls' 7 4 import { mainSchema as pullListSchema } from '@atcute/tangled/types/repo/listPulls' 8 5 import { rpc } from '../client' 9 6 import { removeNullCursor } from '../utils' 10 - 11 - export type Pull = Omit<PullListResponse['items'][number], 'value'> & { 12 - value: TangledPull 13 - } 14 - 15 - export type PullList = { 16 - items: Pull[] 17 - cursor?: string 18 - } 7 + import type { PullList } from './types' 8 + import { validatePull } from './validators' 19 9 20 10 export type ListPullsOptions = { 21 11 cursor?: string ··· 42 32 throw new Error(`Bobbin returned an invalid pull list: ${listValidation.message}`) 43 33 } 44 34 45 - const items = listValidation.value.items.map((item, index) => { 46 - const valueValidation = safeParse(pullSchema, item.value) 47 - if (!valueValidation.ok) { 48 - throw new Error( 49 - `Bobbin returned an invalid pull record at index ${index}: ${valueValidation.message}`, 50 - ) 51 - } 52 - 53 - return { ...item, value: valueValidation.value } 54 - }) 35 + const items = listValidation.value.items.map(validatePull) 55 36 56 37 return { items, cursor: listValidation.value.cursor } 57 38 }
+3 -1
src/lib/tangled/repo/listRepos.ts
··· 4 4 import { mainSchema as reposListSchema } from '@atcute/tangled/types/repo/listRepos' 5 5 import { rpc } from '../client' 6 6 import { removeNullCursor } from '../utils' 7 - import { getRepoRkey, validateRepo, type Repo, type RepoList } from './types' 7 + import { getRepoRkey } from './repoIdentity' 8 + import type { Repo, RepoList } from './types' 9 + import { validateRepo } from './validators' 8 10 9 11 export async function listRepos(did: Did): Promise<RepoList> { 10 12 const response = await ok(
+15
src/lib/tangled/repo/repoIdentity.ts
··· 1 + import { parseResourceUri } from '@atcute/lexicons' 2 + import type { Repo } from './types' 3 + 4 + export function getRepoRkey(repo: Repo): string { 5 + const { rkey } = parseResourceUri(repo.uri) 6 + if (rkey === undefined) { 7 + throw new Error(`Repository URI has no record key: ${repo.uri}`) 8 + } 9 + 10 + return rkey 11 + } 12 + 13 + export function getRepoName(repo: Repo): string { 14 + return repo.value.name ?? getRepoRkey(repo) 15 + }
+23 -43
src/lib/tangled/repo/types.ts
··· 1 1 import type { Cid, ResourceUri } from '@atcute/lexicons' 2 - import { parseResourceUri, safeParse } from '@atcute/lexicons' 3 2 import type { Main as TangledRepo } from '@atcute/tangled/types/repo' 4 - import { mainSchema as repoSchema } from '@atcute/tangled/types/repo' 3 + import type { Main as TangledIssue } from '@atcute/tangled/types/repo/issue' 4 + import type { $output as IssueListResponse } from '@atcute/tangled/types/repo/listIssues' 5 + import type { Main as TangledPull } from '@atcute/tangled/types/repo/pull' 6 + import type { $output as PullListResponse } from '@atcute/tangled/types/repo/listPulls' 5 7 6 - export type Repo = { 8 + export type TangledRecord<TValue> = { 7 9 cid?: Cid 8 10 uri: ResourceUri 9 - value: TangledRepo 11 + value: TValue 10 12 } 11 13 12 - export type RepoList = { 13 - items: Repo[] 14 - cursor?: string 15 - } 14 + export type Repo = TangledRecord<TangledRepo> 16 15 17 - export function getRepoRkey(repo: Repo): string { 18 - const { rkey } = parseResourceUri(repo.uri) 19 - if (rkey === undefined) { 20 - throw new Error(`Repository URI has no record key: ${repo.uri}`) 21 - } 16 + export type IssueRecord = TangledRecord<TangledIssue> 22 17 23 - return rkey 24 - } 25 - 26 - export function getRepoName(repo: Repo): string { 27 - return repo.value.name ?? getRepoRkey(repo) 18 + export type Issue = Omit<IssueListResponse['items'][number], 'value'> & { 19 + value: TangledIssue 28 20 } 29 21 30 - export function validateRepo(repo: { cid?: Cid; uri: ResourceUri; value: unknown }): Repo { 31 - const validation = safeParse(repoSchema, normalizeRepoRecord(repo.value)) 32 - if (!validation.ok) { 33 - throw new Error(`Bobbin returned an invalid repo record: ${repo.uri}: ${validation.message}`) 34 - } 22 + export type PullRecord = TangledRecord<TangledPull> 35 23 36 - return { ...repo, value: validation.value } 24 + export type Pull = Omit<PullListResponse['items'][number], 'value'> & { 25 + value: TangledPull 37 26 } 38 27 39 - function normalizeRepoRecord(value: unknown): unknown { 40 - if (!isRecord(value)) return value 28 + export type RepoList = { 29 + items: Repo[] 30 + cursor?: string 31 + } 41 32 42 - const normalized = { ...value } 43 - if (typeof normalized.description === 'string') { 44 - const graphemes = Array.from( 45 - new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(normalized.description), 46 - ({ segment }) => segment, 47 - ) 48 - 49 - if (graphemes.length === 0) { 50 - delete normalized.description 51 - } else if (graphemes.length > 140) { 52 - normalized.description = graphemes.slice(0, 140).join('') 53 - } 54 - } 55 - 56 - return normalized 33 + export type IssueList = { 34 + items: Issue[] 35 + cursor?: string 57 36 } 58 37 59 - function isRecord(value: unknown): value is Record<string, unknown> { 60 - return typeof value === 'object' && value !== null && !Array.isArray(value) 38 + export type PullList = { 39 + items: Pull[] 40 + cursor?: string 61 41 }
+64
src/lib/tangled/repo/validators.ts
··· 1 + import { safeParse } from '@atcute/lexicons' 2 + import { mainSchema as repoSchema } from '@atcute/tangled/types/repo' 3 + import { mainSchema as issueSchema } from '@atcute/tangled/types/repo/issue' 4 + import { mainSchema as pullSchema } from '@atcute/tangled/types/repo/pull' 5 + import type { Issue, IssueRecord, Pull, PullRecord, Repo, TangledRecord } from './types' 6 + 7 + export function validateRepo(record: TangledRecord<unknown>): Repo { 8 + const validation = safeParse(repoSchema, normalizeRepoRecord(record.value)) 9 + if (!validation.ok) { 10 + throw new Error(`Bobbin returned an invalid repo record: ${record.uri}: ${validation.message}`) 11 + } 12 + 13 + return { ...record, value: validation.value } 14 + } 15 + 16 + export function validateIssueRecord(record: TangledRecord<unknown>): IssueRecord { 17 + const validation = safeParse(issueSchema, record.value) 18 + if (!validation.ok) { 19 + throw new Error(`Bobbin returned an invalid issue record: ${record.uri}: ${validation.message}`) 20 + } 21 + 22 + return { ...record, value: validation.value } 23 + } 24 + 25 + export function validateIssue(item: Omit<Issue, 'value'> & TangledRecord<unknown>): Issue { 26 + return { ...item, value: validateIssueRecord(item).value } 27 + } 28 + 29 + export function validatePullRecord(record: TangledRecord<unknown>): PullRecord { 30 + const validation = safeParse(pullSchema, record.value) 31 + if (!validation.ok) { 32 + throw new Error(`Bobbin returned an invalid pull record: ${record.uri}: ${validation.message}`) 33 + } 34 + 35 + return { ...record, value: validation.value } 36 + } 37 + 38 + export function validatePull(item: Omit<Pull, 'value'> & TangledRecord<unknown>): Pull { 39 + return { ...item, value: validatePullRecord(item).value } 40 + } 41 + 42 + function normalizeRepoRecord(value: unknown): unknown { 43 + if (!isRecord(value)) return value 44 + 45 + const normalized = { ...value } 46 + if (typeof normalized.description === 'string') { 47 + const graphemes = Array.from( 48 + new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(normalized.description), 49 + ({ segment }) => segment, 50 + ) 51 + 52 + if (graphemes.length === 0) { 53 + delete normalized.description 54 + } else if (graphemes.length > 140) { 55 + normalized.description = graphemes.slice(0, 140).join('') 56 + } 57 + } 58 + 59 + return normalized 60 + } 61 + 62 + function isRecord(value: unknown): value is Record<string, unknown> { 63 + return typeof value === 'object' && value !== null && !Array.isArray(value) 64 + }