From a1eb6dcfc467ed780ac53c37d41b7049ea83c05b Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Fri, 2 Oct 2026 14:29:08 +0900 Subject: [PATCH] web: remove bobbin xrpc calls for knot/spindle list Signed-off-by: Seongmin Lee --- web/src/lib/api/count.ts | 4 - web/src/lib/api/repoCreationOptions.ts | 24 ---- web/src/lib/api/repoCreationTargets.test.ts | 109 ----------------- web/src/lib/api/repoCreationTargets.ts | 128 -------------------- web/src/routes/repo/migrate/+page.svelte | 22 +++- web/src/routes/repo/migrate/+page.ts | 5 - web/src/routes/repo/new/+page.ts | 2 - web/src/routes/welcome/+page.ts | 4 - 8 files changed, 19 insertions(+), 279 deletions(-) delete mode 100644 web/src/lib/api/repoCreationOptions.ts delete mode 100644 web/src/lib/api/repoCreationTargets.test.ts delete mode 100644 web/src/lib/api/repoCreationTargets.ts delete mode 100644 web/src/routes/repo/migrate/+page.ts diff --git a/web/src/lib/api/count.ts b/web/src/lib/api/count.ts index e90d0dfa2..01ee866ae 100644 --- a/web/src/lib/api/count.ts +++ b/web/src/lib/api/count.ts @@ -13,7 +13,6 @@ export type CountName = | "sh.tangled.graph.countFollowsBy" | "sh.tangled.graph.countVouches" | "sh.tangled.graph.countVouchesBy" - | "sh.tangled.knot.countKnots" | "sh.tangled.label.countDefinitions" | "sh.tangled.label.countOps" | "sh.tangled.label.countOpsBy" @@ -32,9 +31,6 @@ export type CountName = | "sh.tangled.repo.issue.countStatesBy" | "sh.tangled.repo.pull.countStatuses" | "sh.tangled.repo.pull.countStatusesBy" - | "sh.tangled.spindle.countSpindles" - | "sh.tangled.spindle.countMembers" - | "sh.tangled.spindle.countMembersBy" | "sh.tangled.string.countStrings"; export interface CountResult { diff --git a/web/src/lib/api/repoCreationOptions.ts b/web/src/lib/api/repoCreationOptions.ts deleted file mode 100644 index f99557fa3..000000000 --- a/web/src/lib/api/repoCreationOptions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Did } from "@atcute/lexicons/syntax"; -import { createBobbinClient } from "$lib/api/client"; -import { stream } from "$lib/api/load"; -import { availableKnots, availableSpindles } from "$lib/api/repoCreationTargets"; - -interface RepoCreationParent { - auth?: { did: string } | null; - publicConfig: { bobbinUrl: string }; -} - -export const repoCreationOptions = ( - parent: RepoCreationParent, - fetch: typeof globalThis.fetch -): { knots: Promise; spindles: Promise } => { - if (!parent.auth?.did) { - return { knots: Promise.resolve([]), spindles: Promise.resolve([]) }; - } - const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch }); - const did = parent.auth.did as Did; - return { - knots: stream(availableKnots(ctx, did).catch(() => [])), - spindles: stream(availableSpindles(ctx, did).catch(() => [])) - }; -}; diff --git a/web/src/lib/api/repoCreationTargets.test.ts b/web/src/lib/api/repoCreationTargets.test.ts deleted file mode 100644 index 8cff13a3d..000000000 --- a/web/src/lib/api/repoCreationTargets.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import type { Did } from "@atcute/lexicons/syntax"; -import { createBobbinClient, type BobbinContext } from "$lib/api/client"; -import { availableKnots, availableSpindles } from "$lib/api/repoCreationTargets"; - -const did = "did:plc:alice" as Did; - -describe("availableSpindles", () => { - it("merges owned spindles and memberships across pages", async () => { - const responses = [ - { - items: [ - { - uri: "at://did:plc:alice/sh.tangled.spindle/owned.example", - value: {} - } - ], - cursor: "next" - }, - { - items: [ - { - uri: "at://did:plc:owner/sh.tangled.spindle.member/one", - value: { instance: "member.example" } - }, - { - uri: "at://did:plc:owner/sh.tangled.spindle.member/two", - value: { instance: "owned.example" } - }, - { uri: "at://did:plc:owner/sh.tangled.spindle.member/bad", value: {} } - ] - }, - { - items: [ - { - uri: "at://did:plc:alice/sh.tangled.spindle/another.example", - value: {} - } - ] - } - ]; - const call = vi.fn(async (_schema: unknown, _options: unknown) => ({ - ok: true, - data: responses.shift() - })); - const ctx = { xrpc: { call } } as unknown as BobbinContext; - - await expect(availableSpindles(ctx, did)).resolves.toEqual([ - "another.example", - "member.example", - "owned.example" - ]); - expect(call).toHaveBeenCalledTimes(3); - expect(call.mock.calls[0][1]).toMatchObject({ params: { subject: did, limit: 1_000 } }); - expect(call.mock.calls[2][1]).toMatchObject({ params: { cursor: "next" } }); - }); -}); - -describe("availableKnots", () => { - it("merges owned knots and memberships", async () => { - const pages: Record = { - "/xrpc/sh.tangled.knot.listKnots": { - items: [{ uri: "at://did:plc:alice/sh.tangled.knot/owned.example", value: {} }] - }, - "/xrpc/sh.tangled.knot.listMembers": { - items: [ - { - uri: "at://did:plc:owner/sh.tangled.knot.member/one", - value: { domain: "member.example" } - }, - { - uri: "at://did:web:member.example/sh.tangled.bobbin.knotMember/did:plc:alice", - value: { - subject: "did:plc:alice", - addedBy: "did:plc:owner", - createdAt: "2026-06-01T00:00:00.000Z" - } - }, - { - uri: "at://did:plc:owner/sh.tangled.knot.member/two", - value: { domain: "owned.example" } - }, - { uri: "at://did:plc:owner/sh.tangled.knot.member/bad", value: {} } - ] - } - }; - const fetch = vi.fn(async (input) => { - const asked = new URL(String(input)); - const page = pages[asked.pathname]; - return new Response(JSON.stringify(page ?? { error: "MethodNotImplemented" }), { - status: page ? 200 : 501, - headers: { "content-type": "application/json" } - }); - }); - const ctx = createBobbinClient({ serviceUrl: "https://bobbin.oyster.cafe", fetch }); - - await expect(availableKnots(ctx, did)).resolves.toEqual([ - "member.example", - "owned.example" - ]); - expect(fetch.mock.calls.map(([input]) => new URL(String(input)).pathname)).toEqual([ - "/xrpc/sh.tangled.knot.listKnots", - "/xrpc/sh.tangled.knot.listMembers" - ]); - expect(String(fetch.mock.calls[1][0])).toBe( - "https://bobbin.oyster.cafe/xrpc/sh.tangled.knot.listMembers?subject=did%3Aplc%3Aalice&limit=1000" - ); - }); -}); diff --git a/web/src/lib/api/repoCreationTargets.ts b/web/src/lib/api/repoCreationTargets.ts deleted file mode 100644 index 0e5207d02..000000000 --- a/web/src/lib/api/repoCreationTargets.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ok } from "@atcute/client"; -import type { Did } from "@atcute/lexicons/syntax"; -import type { BobbinContext } from "$lib/api/client"; -import { jsonGet } from "$lib/api/_request"; -import { mainSchema as listKnotsSchema } from "$lib/api/lexicons/types/sh/tangled/knot/listKnots"; -import { mainSchema as listSpindleMembersSchema } from "$lib/api/lexicons/types/sh/tangled/spindle/listMembers"; -import { mainSchema as listSpindlesSchema } from "$lib/api/lexicons/types/sh/tangled/spindle/listSpindles"; -import { parseResourceUri } from "@atcute/lexicons/syntax"; -import { hostForServiceDid } from "$lib/auth/agent"; - -interface Page { - items: T[]; - cursor?: string; -} - -interface RecordItem { - uri: string; - value: unknown; -} - -const allPages = async (load: (cursor?: string) => Promise>): Promise => { - const items: T[] = []; - let cursor: string | undefined; - do { - const page = await load(cursor); - items.push(...page.items); - cursor = page.cursor || undefined; - } while (cursor); - return items; -}; - -const knotDomain = (value: unknown): string | null => { - if (typeof value !== "object" || value === null || !("domain" in value)) return null; - return typeof value.domain === "string" && value.domain ? value.domain : null; -}; - -const repoFromUri = (uri: unknown): string | null => { - if (typeof uri !== "string") return null; - try { - return parseResourceUri(uri).repo; - } catch { - return null; - } -}; - -const recordName = (uri: unknown): string | null => { - if (typeof uri !== "string") return null; - try { - return parseResourceUri(uri).rkey ?? null; - } catch { - return null; - } -}; - -const knotMembershipHost = (item: unknown): string | null => { - if (typeof item !== "object" || item === null) return null; - const record = item as { uri?: unknown; value?: unknown }; - const repo = repoFromUri(record.uri); - const host = repo ? hostForServiceDid(repo) : null; - if (host) return host; - if (record.value !== undefined) { - const domain = knotDomain(record.value); - if (domain) return domain; - } - return null; -}; - -const spindleInstance = (value: unknown): string | null => { - if (typeof value !== "object" || value === null || !("instance" in value)) return null; - return typeof value.instance === "string" && value.instance ? value.instance : null; -}; - -const targetNames = ( - owned: RecordItem[], - memberships: RecordItem[], - memberName: (item: RecordItem) => string | null -): string[] => { - const names = new Set(); - for (const item of owned) { - const name = recordName(item.uri); - if (name) names.add(name); - } - for (const item of memberships) { - const name = memberName(item); - if (name) names.add(name); - } - return [...names].sort((a, b) => a.localeCompare(b)); -}; - -export const availableKnots = async (ctx: BobbinContext, did: string): Promise => { - const [owned, memberships] = await Promise.all([ - allPages((cursor) => - ok( - ctx.xrpc.call(listKnotsSchema, { - params: { subject: did as Did, limit: 1_000, cursor } - }) - ) - ), - allPages((cursor) => - jsonGet>(ctx, "sh.tangled.knot.listMembers", { - subject: did, - limit: 1_000, - cursor - }) - ) - ]); - return targetNames(owned, memberships, knotMembershipHost); -}; - -export const availableSpindles = async (ctx: BobbinContext, did: string): Promise => { - const [owned, memberships] = await Promise.all([ - allPages((cursor) => - ok( - ctx.xrpc.call(listSpindlesSchema, { - params: { subject: did as Did, limit: 1_000, cursor } - }) - ) - ), - allPages((cursor) => - ok( - ctx.xrpc.call(listSpindleMembersSchema, { - params: { subject: did as Did, limit: 1_000, cursor } - }) - ) - ) - ]); - return targetNames(owned, memberships, (item) => spindleInstance(item.value)); -}; diff --git a/web/src/routes/repo/migrate/+page.svelte b/web/src/routes/repo/migrate/+page.svelte index 209f13843..a38bcee90 100644 --- a/web/src/routes/repo/migrate/+page.svelte +++ b/web/src/routes/repo/migrate/+page.svelte @@ -11,7 +11,9 @@ } from "$lib/components/import/UrlRepoImporter.svelte"; import Card from "$lib/components/ui/Card.svelte"; import { getAuth } from "$lib/auth.svelte"; + import { createBobbinClient } from "$lib/api/client"; import { migrationRunning } from "$lib/api/migrator"; + import { getPreferences } from "$lib/api/preferences"; import type { PageData } from "./$types"; let { data }: { data: PageData } = $props(); @@ -20,6 +22,20 @@ const auth = getAuth(); const user = $derived(auth.currentUser); + const knots = $derived.by(async () => { + const saved = user + ? await auth + .agentFor(user.did, { relogin: false }) + .then((agent) => + getPreferences(createBobbinClient({ serviceUrl: auth.bobbinUrl, agent })) + ) + .then((prefs) => prefs.knots) + .catch(() => []) + : []; + return [ + ...new Set([data.publicConfig.primaryKnotHostname ?? "", ...saved].filter(Boolean)) + ]; + }); const urlRunning = $derived( urlMigration.status === "submitting" || @@ -52,7 +68,7 @@ {/snippet} -
+