From a64f87194d3988b247cb1af94f4e0480a372ec1c Mon Sep 17 00:00:00 2001 From: eti Date: Tue, 14 Jul 2026 02:37:55 +0200 Subject: [PATCH] web: switch prettier to double quotes Signed-off-by: eti --- web/lex.config.ts | 42 +++---- web/playwright.config.ts | 6 +- web/prettier.config.js | 10 +- web/src/hooks.server.ts | 4 +- web/src/icon.d.ts | 8 +- web/src/lib/api/_request.ts | 8 +- web/src/lib/api/client.test.ts | 84 ++++++------- web/src/lib/api/client.ts | 8 +- web/src/lib/api/count.ts | 84 ++++++------- web/src/lib/api/coverage.ts | 6 +- web/src/lib/api/graph.ts | 40 +++---- web/src/lib/api/identity.test.ts | 64 +++++----- web/src/lib/api/identity.ts | 8 +- web/src/lib/api/index.ts | 22 ++-- web/src/lib/api/knot.test.ts | 18 +-- web/src/lib/api/knot.ts | 60 +++++----- web/src/lib/api/load.test.ts | 98 +++++++-------- web/src/lib/api/load.ts | 18 +-- web/src/lib/api/pagination.test.ts | 58 ++++----- web/src/lib/api/pagination.ts | 16 +-- web/src/lib/api/profile.ts | 16 +-- web/src/lib/api/records.ts | 30 ++--- web/src/lib/api/search.ts | 10 +- web/src/lib/api/uri.ts | 6 +- web/src/lib/auth.svelte.ts | 64 +++++----- web/src/lib/auth/accounts.ts | 20 ++-- web/src/lib/auth/agent.ts | 10 +- web/src/lib/auth/guards.ts | 12 +- .../profile/FollowerFollowing.svelte | 8 +- .../lib/components/profile/counts.svelte.ts | 20 ++-- .../profile/tabs/OverviewTab.svelte | 8 +- .../components/profile/tabs/PeopleTab.svelte | 6 +- .../profile/tabs/StringListTab.svelte | 6 +- .../components/profile/tabs/VouchTab.svelte | 6 +- web/src/lib/components/profile/types.ts | 6 +- web/src/lib/components/ui/Spinner.svelte | 4 +- web/src/lib/format.ts | 34 +++--- web/src/lib/server/config.ts | 14 +-- web/src/routes/+layout.server.ts | 8 +- web/src/routes/+page.svelte | 4 +- web/src/routes/[handle]/+layout.ts | 42 +++---- web/src/routes/[handle]/+page.svelte | 26 ++-- web/src/routes/[handle]/+page.ts | 112 +++++++++--------- web/src/routes/about/+page.svelte | 2 +- web/svelte.config.js | 6 +- 45 files changed, 571 insertions(+), 571 deletions(-) diff --git a/web/lex.config.ts b/web/lex.config.ts index 9c3b31a8..e5851854 100644 --- a/web/lex.config.ts +++ b/web/lex.config.ts @@ -1,30 +1,30 @@ -import { defineLexiconConfig } from '@atcute/lex-cli'; +import { defineLexiconConfig } from "@atcute/lex-cli"; // generate sh.tangled types; resolve com.atproto refs from @atcute/atproto. export default defineLexiconConfig({ - formatter: { type: 'prettier' }, + formatter: { type: "prettier" }, generate: { - outdir: 'src/lib/api/lexicons/', + outdir: "src/lib/api/lexicons/", clean: true, - imports: ['@atcute/atproto'], + imports: ["@atcute/atproto"], files: [ - '../lexicons/*.json', - '../lexicons/actor/**/*.json', - '../lexicons/ci/**/*.json', - '../lexicons/feed/**/*.json', - '../lexicons/git/**/*.json', - '../lexicons/graph/**/*.json', - '../lexicons/issue/**/*.json', - '../lexicons/knot/**/*.json', - '../lexicons/label/**/*.json', - '../lexicons/markup/**/*.json', - '../lexicons/pipeline/**/*.json', - '../lexicons/pulls/**/*.json', - '../lexicons/repo/**/*.json', - '../lexicons/spindle/**/*.json', - '../lexicons/string/**/*.json', - '../lexicons/sync/**/*.json', - '../bobbin/crates/types/lexicons/**/*.json' + "../lexicons/*.json", + "../lexicons/actor/**/*.json", + "../lexicons/ci/**/*.json", + "../lexicons/feed/**/*.json", + "../lexicons/git/**/*.json", + "../lexicons/graph/**/*.json", + "../lexicons/issue/**/*.json", + "../lexicons/knot/**/*.json", + "../lexicons/label/**/*.json", + "../lexicons/markup/**/*.json", + "../lexicons/pipeline/**/*.json", + "../lexicons/pulls/**/*.json", + "../lexicons/repo/**/*.json", + "../lexicons/spindle/**/*.json", + "../lexicons/string/**/*.json", + "../lexicons/sync/**/*.json", + "../bobbin/crates/types/lexicons/**/*.json" ] } }); diff --git a/web/playwright.config.ts b/web/playwright.config.ts index 5334e45f..cbb70fd0 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from '@playwright/test'; +import { defineConfig } from "@playwright/test"; export default defineConfig({ - webServer: { command: 'pnpm run build && pnpm run preview', port: 4173 }, - testMatch: '**/*.e2e.{ts,js}' + webServer: { command: "pnpm run build && pnpm run preview", port: 4173 }, + testMatch: "**/*.e2e.{ts,js}" }); diff --git a/web/prettier.config.js b/web/prettier.config.js index f8754884..6ec7ab57 100644 --- a/web/prettier.config.js +++ b/web/prettier.config.js @@ -1,12 +1,12 @@ /** @type {import("prettier").Config} */ const config = { useTabs: true, - singleQuote: true, - trailingComma: 'none', + singleQuote: false, + trailingComma: "none", printWidth: 100, - plugins: ['prettier-plugin-svelte', 'prettier-plugin-tailwindcss'], - overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }], - tailwindStylesheet: './src/app.css' + plugins: ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + overrides: [{ files: "*.svelte", options: { parser: "svelte" } }], + tailwindStylesheet: "./src/app.css" }; export default config; diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts index 9accbff7..b0aa076f 100644 --- a/web/src/hooks.server.ts +++ b/web/src/hooks.server.ts @@ -1,4 +1,4 @@ -import type { Handle } from '@sveltejs/kit'; +import type { Handle } from "@sveltejs/kit"; export const handle: Handle = async ({ event, resolve }) => { return resolve(event, { @@ -9,7 +9,7 @@ export const handle: Handle = async ({ event, resolve }) => { // the data we already had. // so we allow these headers to have atcute function properly. filterSerializedResponseHeaders(name) { - return name === 'content-type' || name === 'content-length'; + return name === "content-type" || name === "content-length"; } }); }; diff --git a/web/src/icon.d.ts b/web/src/icon.d.ts index a2bd4c78..ca79e9b5 100644 --- a/web/src/icon.d.ts +++ b/web/src/icon.d.ts @@ -1,11 +1,11 @@ // `$icon/` is a vite alias for unplugin-icons' `~icons/lucide/` // virtual module (see vite.config.ts). mirror its ambient type here so tsc / // svelte-check resolve the aliased imports. must stay a global .d.ts (no export) -declare module '$icon/*' { - import type { Component } from 'svelte'; - import type { SvelteHTMLElements } from 'svelte/elements'; +declare module "$icon/*" { + import type { Component } from "svelte"; + import type { SvelteHTMLElements } from "svelte/elements"; - const component: Component; + const component: Component; export default component; } diff --git a/web/src/lib/api/_request.ts b/web/src/lib/api/_request.ts index 3d51d47c..4567fac9 100644 --- a/web/src/lib/api/_request.ts +++ b/web/src/lib/api/_request.ts @@ -1,5 +1,5 @@ -import { ClientResponseError, isXRPCErrorPayload, type XRPCErrorPayload } from '@atcute/client'; -import type { BobbinContext, QueryValue, XrpcRequestInit } from './client'; +import { ClientResponseError, isXRPCErrorPayload, type XRPCErrorPayload } from "@atcute/client"; +import type { BobbinContext, QueryValue, XrpcRequestInit } from "./client"; export const buildUrl = ( origin: string, @@ -21,7 +21,7 @@ export const buildUrl = ( }; export const toResponseError = async (response: Response): Promise => { - let data: XRPCErrorPayload = { error: 'XRPCError', message: response.statusText }; + let data: XRPCErrorPayload = { error: "XRPCError", message: response.statusText }; try { const body: unknown = await response.json(); if (isXRPCErrorPayload(body)) data = body; @@ -38,7 +38,7 @@ export const jsonGet = async ( init?: XrpcRequestInit ): Promise => { const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid, params), { - headers: { accept: 'application/json', ...init?.headers }, + headers: { accept: "application/json", ...init?.headers }, signal: init?.signal }); if (!response.ok) throw await toResponseError(response); diff --git a/web/src/lib/api/client.test.ts b/web/src/lib/api/client.test.ts index bbfb605a..ba6d7aca 100644 --- a/web/src/lib/api/client.test.ts +++ b/web/src/lib/api/client.test.ts @@ -1,106 +1,106 @@ -import { describe, expect, it, vi, type Mock } from 'vitest'; -import { ClientResponseError, createBobbinClient, type BobbinContext } from './client'; -import { jsonGet, rawGet } from './_request'; +import { describe, expect, it, vi, type Mock } from "vitest"; +import { ClientResponseError, createBobbinClient, type BobbinContext } from "./client"; +import { jsonGet, rawGet } from "./_request"; const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => new Response(JSON.stringify(body), { status: 200, - headers: { 'content-type': 'application/json' }, + headers: { "content-type": "application/json" }, ...init }); const makeCtx = ( fetchMock: typeof globalThis.fetch, - serviceUrl = 'https://bobbin.test' + serviceUrl = "https://bobbin.test" ): BobbinContext => createBobbinClient({ serviceUrl, fetch: fetchMock }); const fetchedUrl = (mock: Mock, n = 0): URL => new URL(String(mock.mock.calls[n][0])); -describe('createBobbinClient', () => { - it('normalizes trailing slashes off the service url', () => { - const ctx = makeCtx(vi.fn(), 'https://bobbin.test///'); - expect(ctx.serviceUrl).toBe('https://bobbin.test'); +describe("createBobbinClient", () => { + it("normalizes trailing slashes off the service url", () => { + const ctx = makeCtx(vi.fn(), "https://bobbin.test///"); + expect(ctx.serviceUrl).toBe("https://bobbin.test"); }); }); -describe('jsonGet URL construction', () => { - it('appends an array param as repeated keys', async () => { +describe("jsonGet URL construction", () => { + it("appends an array param as repeated keys", async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ ok: 1 })); - await jsonGet(makeCtx(fetchMock), 'sh.tangled.repo.getRepos', { repos: ['a', 'b'] }); + await jsonGet(makeCtx(fetchMock), "sh.tangled.repo.getRepos", { repos: ["a", "b"] }); const url = fetchedUrl(fetchMock); - expect(url.searchParams.getAll('repos')).toEqual(['a', 'b']); - expect(url.search).toBe('?repos=a&repos=b'); + expect(url.searchParams.getAll("repos")).toEqual(["a", "b"]); + expect(url.search).toBe("?repos=a&repos=b"); }); - it('omits undefined params entirely', async () => { + it("omits undefined params entirely", async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ ok: 1 })); - await jsonGet(makeCtx(fetchMock), 'sh.tangled.x', { keep: 'yes', drop: undefined }); + await jsonGet(makeCtx(fetchMock), "sh.tangled.x", { keep: "yes", drop: undefined }); const url = fetchedUrl(fetchMock); - expect(url.searchParams.has('drop')).toBe(false); - expect(url.searchParams.get('keep')).toBe('yes'); + expect(url.searchParams.has("drop")).toBe(false); + expect(url.searchParams.get("keep")).toBe("yes"); }); - it('resolves the query against the normalized origin', async () => { + it("resolves the query against the normalized origin", async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ ok: 1 })); - await jsonGet(makeCtx(fetchMock, 'https://bobbin.test/'), 'sh.tangled.x', { a: '1' }); - expect(fetchedUrl(fetchMock).href).toBe('https://bobbin.test/xrpc/sh.tangled.x?a=1'); + await jsonGet(makeCtx(fetchMock, "https://bobbin.test/"), "sh.tangled.x", { a: "1" }); + expect(fetchedUrl(fetchMock).href).toBe("https://bobbin.test/xrpc/sh.tangled.x?a=1"); }); }); -describe('jsonGet request headers & signal', () => { - it('sends accept: application/json and merges caller headers + signal', async () => { +describe("jsonGet request headers & signal", () => { + it("sends accept: application/json and merges caller headers + signal", async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ ok: 1 })); const controller = new AbortController(); - await jsonGet(makeCtx(fetchMock), 'sh.tangled.x', undefined, { - headers: { 'x-custom': '1' }, + await jsonGet(makeCtx(fetchMock), "sh.tangled.x", undefined, { + headers: { "x-custom": "1" }, signal: controller.signal }); const init = fetchMock.mock.calls[0][1] as RequestInit; - expect(init.headers).toMatchObject({ accept: 'application/json', 'x-custom': '1' }); + expect(init.headers).toMatchObject({ accept: "application/json", "x-custom": "1" }); expect(init.signal).toBe(controller.signal); }); }); -describe('jsonGet responses', () => { - it('throws ClientResponseError carrying status + error/description for a JSON error body', async () => { +describe("jsonGet responses", () => { + it("throws ClientResponseError carrying status + error/description for a JSON error body", async () => { const fetchMock = vi .fn() .mockResolvedValue( - jsonResponse({ error: 'RecordNotFound', message: 'no such repo' }, { status: 404 }) + jsonResponse({ error: "RecordNotFound", message: "no such repo" }, { status: 404 }) ); - const err = await jsonGet(makeCtx(fetchMock), 'sh.tangled.x').catch((e: unknown) => e); + const err = await jsonGet(makeCtx(fetchMock), "sh.tangled.x").catch((e: unknown) => e); expect(err).toBeInstanceOf(ClientResponseError); const cre = err as ClientResponseError; expect(cre.status).toBe(404); - expect(cre.error).toBe('RecordNotFound'); - expect(cre.description).toBe('no such repo'); + expect(cre.error).toBe("RecordNotFound"); + expect(cre.description).toBe("no such repo"); }); - it('falls back to the status line for a non-JSON error body (does not hang)', async () => { + it("falls back to the status line for a non-JSON error body (does not hang)", async () => { const fetchMock = vi.fn().mockResolvedValue( - new Response('502 Bad Gateway', { + new Response("502 Bad Gateway", { status: 502, - statusText: 'Bad Gateway' + statusText: "Bad Gateway" }) ); - const err = await jsonGet(makeCtx(fetchMock), 'sh.tangled.x').catch((e: unknown) => e); + const err = await jsonGet(makeCtx(fetchMock), "sh.tangled.x").catch((e: unknown) => e); expect(err).toBeInstanceOf(ClientResponseError); const cre = err as ClientResponseError; expect(cre.status).toBe(502); - expect(cre.error).toBe('XRPCError'); - expect(cre.description).toBe('Bad Gateway'); + expect(cre.error).toBe("XRPCError"); + expect(cre.description).toBe("Bad Gateway"); }); }); -describe('rawGet', () => { - it('throws ClientResponseError on a non-2xx status', async () => { +describe("rawGet", () => { + it("throws ClientResponseError on a non-2xx status", async () => { const fetchMock = vi .fn() .mockResolvedValue( - jsonResponse({ error: 'UpstreamFailed', message: 'knot down' }, { status: 502 }) + jsonResponse({ error: "UpstreamFailed", message: "knot down" }, { status: 502 }) ); - const err = await rawGet(makeCtx(fetchMock), 'sh.tangled.repo.archive').catch( + const err = await rawGet(makeCtx(fetchMock), "sh.tangled.repo.archive").catch( (e: unknown) => e ); expect(err).toBeInstanceOf(ClientResponseError); diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index 2537d585..7cdbfc6b 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -1,7 +1,7 @@ -import { Client, simpleFetchHandler } from '@atcute/client'; +import { Client, simpleFetchHandler } from "@atcute/client"; -export { ClientResponseError, isXRPCErrorPayload, ok } from '@atcute/client'; -export type { XRPCErrorPayload } from '@atcute/client'; +export { ClientResponseError, isXRPCErrorPayload, ok } from "@atcute/client"; +export type { XRPCErrorPayload } from "@atcute/client"; // generated lexicons register ambient types; don't import the barrel at runtime. @@ -17,7 +17,7 @@ export interface CreateBobbinOptions { } export const createBobbinClient = ({ serviceUrl, fetch }: CreateBobbinOptions): BobbinContext => { - const origin = serviceUrl.replace(/\/+$/, ''); + const origin = serviceUrl.replace(/\/+$/, ""); const boundFetch = fetch ?? globalThis.fetch; const xrpc = new Client({ handler: simpleFetchHandler({ service: origin, fetch: boundFetch }) }); return { xrpc, serviceUrl: origin, fetch: boundFetch }; diff --git a/web/src/lib/api/count.ts b/web/src/lib/api/count.ts index 75e665d4..401b5ad5 100644 --- a/web/src/lib/api/count.ts +++ b/web/src/lib/api/count.ts @@ -1,48 +1,48 @@ -import type { BobbinContext, XrpcRequestInit } from './client'; -import { jsonGet } from './_request'; +import type { BobbinContext, XrpcRequestInit } from "./client"; +import { jsonGet } from "./_request"; // count endpoints share { subject } params and { count, distinctAuthors } output. export type CountName = - | 'sh.tangled.feed.countStars' - | 'sh.tangled.feed.countStarsBy' - | 'sh.tangled.feed.countComments' - | 'sh.tangled.feed.countCommentsBy' - | 'sh.tangled.feed.countReactions' - | 'sh.tangled.feed.countReactionsBy' - | 'sh.tangled.graph.countFollows' - | 'sh.tangled.graph.countFollowsBy' - | 'sh.tangled.graph.countVouches' - | 'sh.tangled.graph.countVouchesBy' - | 'sh.tangled.git.countRefUpdates' - | 'sh.tangled.git.countRefUpdatesBy' - | 'sh.tangled.knot.countKnots' - | 'sh.tangled.knot.countMembers' - | 'sh.tangled.knot.countMembersBy' - | 'sh.tangled.label.countDefinitions' - | 'sh.tangled.label.countOps' - | 'sh.tangled.label.countOpsBy' - | 'sh.tangled.pipeline.countPipelines' - | 'sh.tangled.pipeline.countPipelinesBy' - | 'sh.tangled.pipeline.countStatuses' - | 'sh.tangled.pipeline.countStatusesBy' - | 'sh.tangled.publicKey.countKeys' - | 'sh.tangled.repo.countArtifacts' - | 'sh.tangled.repo.countArtifactsBy' - | 'sh.tangled.repo.countCollaborators' - | 'sh.tangled.repo.countCollaboratorsBy' - | 'sh.tangled.repo.countIssues' - | 'sh.tangled.repo.countIssuesBy' - | 'sh.tangled.repo.countPulls' - | 'sh.tangled.repo.countPullsBy' - | 'sh.tangled.repo.countRepos' - | 'sh.tangled.repo.issue.countStates' - | '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'; + | "sh.tangled.feed.countStars" + | "sh.tangled.feed.countStarsBy" + | "sh.tangled.feed.countComments" + | "sh.tangled.feed.countCommentsBy" + | "sh.tangled.feed.countReactions" + | "sh.tangled.feed.countReactionsBy" + | "sh.tangled.graph.countFollows" + | "sh.tangled.graph.countFollowsBy" + | "sh.tangled.graph.countVouches" + | "sh.tangled.graph.countVouchesBy" + | "sh.tangled.git.countRefUpdates" + | "sh.tangled.git.countRefUpdatesBy" + | "sh.tangled.knot.countKnots" + | "sh.tangled.knot.countMembers" + | "sh.tangled.knot.countMembersBy" + | "sh.tangled.label.countDefinitions" + | "sh.tangled.label.countOps" + | "sh.tangled.label.countOpsBy" + | "sh.tangled.pipeline.countPipelines" + | "sh.tangled.pipeline.countPipelinesBy" + | "sh.tangled.pipeline.countStatuses" + | "sh.tangled.pipeline.countStatusesBy" + | "sh.tangled.publicKey.countKeys" + | "sh.tangled.repo.countArtifacts" + | "sh.tangled.repo.countArtifactsBy" + | "sh.tangled.repo.countCollaborators" + | "sh.tangled.repo.countCollaboratorsBy" + | "sh.tangled.repo.countIssues" + | "sh.tangled.repo.countIssuesBy" + | "sh.tangled.repo.countPulls" + | "sh.tangled.repo.countPullsBy" + | "sh.tangled.repo.countRepos" + | "sh.tangled.repo.issue.countStates" + | "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 { count: number; diff --git a/web/src/lib/api/coverage.ts b/web/src/lib/api/coverage.ts index 876eca09..bc8791eb 100644 --- a/web/src/lib/api/coverage.ts +++ b/web/src/lib/api/coverage.ts @@ -1,5 +1,5 @@ -import type { BobbinContext, XrpcRequestInit } from './client'; -import { jsonGet } from './_request'; +import type { BobbinContext, XrpcRequestInit } from "./client"; +import { jsonGet } from "./_request"; export interface Coverage { ready: boolean; @@ -8,4 +8,4 @@ export interface Coverage { } export const getCoverage = (ctx: BobbinContext, init?: XrpcRequestInit): Promise => - jsonGet(ctx, 'sh.tangled.bobbin.getCoverage', undefined, init); + jsonGet(ctx, "sh.tangled.bobbin.getCoverage", undefined, init); diff --git a/web/src/lib/api/graph.ts b/web/src/lib/api/graph.ts index c3de2f71..24a75d92 100644 --- a/web/src/lib/api/graph.ts +++ b/web/src/lib/api/graph.ts @@ -1,22 +1,22 @@ -import { ok } from '@atcute/client'; -import { mainSchema as createRecordSchema } from '@atcute/atproto/types/repo/createRecord'; -import { mainSchema as deleteRecordSchema } from '@atcute/atproto/types/repo/deleteRecord'; -import type { Did, Nsid, RecordKey } from '@atcute/lexicons/syntax'; -import type { OAuthUserAgent } from '@atcute/oauth-browser-client'; -import { createClient } from '$lib/auth/agent'; -import type { BobbinContext } from './client'; -import { items } from './pagination'; -import { rkeyFromUri } from './uri'; -import type * as ShTangledGraphFollow from './lexicons/types/sh/tangled/graph/follow'; -import type * as ShTangledGraphVouch from './lexicons/types/sh/tangled/graph/vouch'; -import type * as ShTangledFeedStar from './lexicons/types/sh/tangled/feed/star'; +import { ok } from "@atcute/client"; +import { mainSchema as createRecordSchema } from "@atcute/atproto/types/repo/createRecord"; +import { mainSchema as deleteRecordSchema } from "@atcute/atproto/types/repo/deleteRecord"; +import type { Did, Nsid, RecordKey } from "@atcute/lexicons/syntax"; +import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; +import { createClient } from "$lib/auth/agent"; +import type { BobbinContext } from "./client"; +import { items } from "./pagination"; +import { rkeyFromUri } from "./uri"; +import type * as ShTangledGraphFollow from "./lexicons/types/sh/tangled/graph/follow"; +import type * as ShTangledGraphVouch from "./lexicons/types/sh/tangled/graph/vouch"; +import type * as ShTangledFeedStar from "./lexicons/types/sh/tangled/feed/star"; export type FollowRecord = ShTangledGraphFollow.Main; export type VouchRecord = ShTangledGraphVouch.Main; export type StarRecord = ShTangledFeedStar.Main; -const FOLLOW_COLLECTION = 'sh.tangled.graph.follow' as Nsid; -const STAR_COLLECTION = 'sh.tangled.feed.star' as Nsid; +const FOLLOW_COLLECTION = "sh.tangled.graph.follow" as Nsid; +const STAR_COLLECTION = "sh.tangled.feed.star" as Nsid; // TODO(bobbin): needs a relation point-lookup (e.g. graph.getFollow?actor=&subject=) // or a `viewer` hydration param on lists; scanning listFollowsBy pages is O(follows). @@ -28,7 +28,7 @@ export const findFollowRkey = async ( ): Promise => { for await (const item of items( ctx, - 'sh.tangled.graph.listFollowsBy', + "sh.tangled.graph.listFollowsBy", { subject: viewerDid as Did }, { maxPages: options.maxPages ?? 10 } )) { @@ -66,7 +66,7 @@ const deleteGenericRecord = async ( export const createFollow = (agent: OAuthUserAgent, subject: string): Promise => createGenericRecord(agent, FOLLOW_COLLECTION, { - $type: 'sh.tangled.graph.follow', + $type: "sh.tangled.graph.follow", subject: subject as Did, createdAt: new Date().toISOString() }); @@ -76,9 +76,9 @@ export const deleteFollow = (agent: OAuthUserAgent, rkey: string): Promise export const createStar = (agent: OAuthUserAgent, repoDid: string): Promise => createGenericRecord(agent, STAR_COLLECTION, { - $type: 'sh.tangled.feed.star', + $type: "sh.tangled.feed.star", subject: { - $type: 'sh.tangled.feed.star#repo', + $type: "sh.tangled.feed.star#repo", did: repoDid as Did }, createdAt: new Date().toISOString() @@ -97,12 +97,12 @@ export const listStarRkeys = async ( const rkeys = new Map(); for await (const item of items( ctx, - 'sh.tangled.feed.listStarsBy', + "sh.tangled.feed.listStarsBy", { subject: viewerDid as Did }, { maxPages: options.maxPages ?? 10 } )) { const value = item.value as StarRecord; - if (value.subject.$type === 'sh.tangled.feed.star#repo') { + if (value.subject.$type === "sh.tangled.feed.star#repo") { rkeys.set(value.subject.did, rkeyFromUri(item.uri)); } } diff --git a/web/src/lib/api/identity.test.ts b/web/src/lib/api/identity.test.ts index 83aeb00d..36268180 100644 --- a/web/src/lib/api/identity.test.ts +++ b/web/src/lib/api/identity.test.ts @@ -1,17 +1,17 @@ -import { describe, expect, it, vi } from 'vitest'; -import { createBobbinClient, type BobbinContext } from './client'; -import { IdentityCache, type MiniDoc } from './identity'; +import { describe, expect, it, vi } from "vitest"; +import { createBobbinClient, type BobbinContext } from "./client"; +import { IdentityCache, type MiniDoc } from "./identity"; -const DOC: MiniDoc = { did: 'did:plc:x', handle: 'alice.test' }; +const DOC: MiniDoc = { did: "did:plc:x", handle: "alice.test" }; const docResponse = (doc: MiniDoc): Response => new Response(JSON.stringify(doc), { status: 200, - headers: { 'content-type': 'application/json' } + headers: { "content-type": "application/json" } }); const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext => - createBobbinClient({ serviceUrl: 'https://bobbin.test', fetch: fetchMock }); + createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); const deferred = (): { promise: Promise; resolve: (value: T) => void } => { let resolve!: (value: T) => void; @@ -21,27 +21,27 @@ const deferred = (): { promise: Promise; resolve: (value: T) => void } => return { promise, resolve }; }; -describe('IdentityCache.resolve', () => { - it('fetches on the first miss then serves the cached document', async () => { +describe("IdentityCache.resolve", () => { + it("fetches on the first miss then serves the cached document", async () => { const fetchMock = vi.fn().mockResolvedValue(docResponse(DOC)); const cache = new IdentityCache(makeCtx(fetchMock)); - const first = await cache.resolve('alice.test'); - const second = await cache.resolve('alice.test'); + const first = await cache.resolve("alice.test"); + const second = await cache.resolve("alice.test"); expect(first).toEqual(DOC); expect(second).toEqual(DOC); expect(fetchMock).toHaveBeenCalledTimes(1); - expect((fetchMock.mock.calls[0][0] as URL).searchParams.get('identifier')).toBe('alice.test'); + expect((fetchMock.mock.calls[0][0] as URL).searchParams.get("identifier")).toBe("alice.test"); }); - it('coalesces concurrent misses for the same key into one in-flight fetch', async () => { + it("coalesces concurrent misses for the same key into one in-flight fetch", async () => { const gate = deferred(); const fetchMock = vi.fn().mockReturnValue(gate.promise); const cache = new IdentityCache(makeCtx(fetchMock)); - const a = cache.resolve('alice.test'); - const b = cache.resolve('alice.test'); + const a = cache.resolve("alice.test"); + const b = cache.resolve("alice.test"); expect(fetchMock).toHaveBeenCalledTimes(1); gate.resolve(docResponse(DOC)); @@ -51,50 +51,50 @@ describe('IdentityCache.resolve', () => { expect(fetchMock).toHaveBeenCalledTimes(1); }); - it('re-fetches after the in-flight promise settles (no permanent stampede lock)', async () => { + it("re-fetches after the in-flight promise settles (no permanent stampede lock)", async () => { const fetchMock = vi.fn().mockResolvedValue(docResponse(DOC)); const cache = new IdentityCache(makeCtx(fetchMock)); - await cache.resolve('bob.test'); - fetchMock.mockResolvedValueOnce(docResponse({ did: 'did:plc:y', handle: 'carol.test' })); - await cache.resolve('carol.test'); + await cache.resolve("bob.test"); + fetchMock.mockResolvedValueOnce(docResponse({ did: "did:plc:y", handle: "carol.test" })); + await cache.resolve("carol.test"); expect(fetchMock).toHaveBeenCalledTimes(2); }); - it('populates both directions of the index from a resolved document', async () => { + it("populates both directions of the index from a resolved document", async () => { const fetchMock = vi.fn().mockResolvedValue(docResponse(DOC)); const cache = new IdentityCache(makeCtx(fetchMock)); - await cache.resolve('alice.test'); - expect(cache.didFor('alice.test')).toBe('did:plc:x'); - expect(cache.handleFor('did:plc:x')).toBe('alice.test'); + await cache.resolve("alice.test"); + expect(cache.didFor("alice.test")).toBe("did:plc:x"); + expect(cache.handleFor("did:plc:x")).toBe("alice.test"); }); }); -describe('IdentityCache.prime & map selection', () => { - it('seeds both directions without any fetch', () => { +describe("IdentityCache.prime & map selection", () => { + it("seeds both directions without any fetch", () => { const fetchMock = vi.fn(); const cache = new IdentityCache(makeCtx(fetchMock)); cache.prime(DOC); - expect(cache.didFor('alice.test')).toBe('did:plc:x'); - expect(cache.handleFor('did:plc:x')).toBe('alice.test'); + expect(cache.didFor("alice.test")).toBe("did:plc:x"); + expect(cache.handleFor("did:plc:x")).toBe("alice.test"); expect(fetchMock).not.toHaveBeenCalled(); }); - it('resolves a DID against the did map and a handle against the handle map', async () => { + it("resolves a DID against the did map and a handle against the handle map", async () => { const fetchMock = vi.fn(); const cache = new IdentityCache(makeCtx(fetchMock)); cache.prime(DOC); - await expect(cache.resolve('did:plc:x')).resolves.toEqual(DOC); - await expect(cache.resolve('alice.test')).resolves.toEqual(DOC); + await expect(cache.resolve("did:plc:x")).resolves.toEqual(DOC); + await expect(cache.resolve("alice.test")).resolves.toEqual(DOC); expect(fetchMock).not.toHaveBeenCalled(); // unknown dids miss the handle map and fetch. - fetchMock.mockResolvedValueOnce(docResponse({ did: 'did:plc:z', handle: 'dan.test' })); - await cache.resolve('did:plc:z'); + fetchMock.mockResolvedValueOnce(docResponse({ did: "did:plc:z", handle: "dan.test" })); + await cache.resolve("did:plc:z"); expect(fetchMock).toHaveBeenCalledTimes(1); - expect((fetchMock.mock.calls[0][0] as URL).searchParams.get('identifier')).toBe('did:plc:z'); + expect((fetchMock.mock.calls[0][0] as URL).searchParams.get("identifier")).toBe("did:plc:z"); }); }); diff --git a/web/src/lib/api/identity.ts b/web/src/lib/api/identity.ts index b94fc128..4594b61e 100644 --- a/web/src/lib/api/identity.ts +++ b/web/src/lib/api/identity.ts @@ -1,5 +1,5 @@ -import type { BobbinContext, XrpcRequestInit } from './client'; -import { jsonGet } from './_request'; +import type { BobbinContext, XrpcRequestInit } from "./client"; +import { jsonGet } from "./_request"; export interface MiniDoc { did: string; @@ -13,7 +13,7 @@ export const resolveMiniDoc = ( identifier: string, init?: XrpcRequestInit ): Promise => - jsonGet(ctx, 'com.bad-example.identity.resolveMiniDoc', { identifier }, init); + jsonGet(ctx, "com.bad-example.identity.resolveMiniDoc", { identifier }, init); // did/handle cache with in-flight de-dupe. export class IdentityCache { @@ -40,7 +40,7 @@ export class IdentityCache { } resolve(identifier: string, init?: XrpcRequestInit): Promise { - const cached = identifier.startsWith('did:') + const cached = identifier.startsWith("did:") ? this.#byDid.get(identifier) : this.#byHandle.get(identifier); if (cached) return Promise.resolve(cached); diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index 59c35913..9e6eb371 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -1,11 +1,11 @@ -export * from './client'; -export * from './pagination'; -export * from './count'; -export * from './records'; -export * as knot from './knot'; -export * from './search'; -export * from './coverage'; -export * from './identity'; -export * from './load'; -export * from './uri'; -export * from './graph'; +export * from "./client"; +export * from "./pagination"; +export * from "./count"; +export * from "./records"; +export * as knot from "./knot"; +export * from "./search"; +export * from "./coverage"; +export * from "./identity"; +export * from "./load"; +export * from "./uri"; +export * from "./graph"; diff --git a/web/src/lib/api/knot.test.ts b/web/src/lib/api/knot.test.ts index 5899916c..2dc7d2a8 100644 --- a/web/src/lib/api/knot.test.ts +++ b/web/src/lib/api/knot.test.ts @@ -1,24 +1,24 @@ -import { describe, expect, it, vi } from 'vitest'; -import * as knot from './knot'; -import { ClientResponseError, createBobbinClient, type BobbinContext } from './client'; +import { describe, expect, it, vi } from "vitest"; +import * as knot from "./knot"; +import { ClientResponseError, createBobbinClient, type BobbinContext } from "./client"; const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => new Response(JSON.stringify(body), { status: 200, - headers: { 'content-type': 'application/json' }, + headers: { "content-type": "application/json" }, ...init }); const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext => - createBobbinClient({ serviceUrl: 'https://bobbin.test', fetch: fetchMock }); + createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); -describe('knot.archive (binary passthrough)', () => { - it('throws ClientResponseError on a non-2xx archive response', async () => { +describe("knot.archive (binary passthrough)", () => { + it("throws ClientResponseError on a non-2xx archive response", async () => { const fetchMock = vi .fn() - .mockResolvedValue(jsonResponse({ error: 'UpstreamGone' }, { status: 502 })); + .mockResolvedValue(jsonResponse({ error: "UpstreamGone" }, { status: 502 })); const err = await knot - .archive(makeCtx(fetchMock), { repo: 'did:plc:x/r', ref: 'main' }) + .archive(makeCtx(fetchMock), { repo: "did:plc:x/r", ref: "main" }) .catch((e: unknown) => e); expect(err).toBeInstanceOf(ClientResponseError); expect((err as ClientResponseError).status).toBe(502); diff --git a/web/src/lib/api/knot.ts b/web/src/lib/api/knot.ts index 540b5743..1e2ccb81 100644 --- a/web/src/lib/api/knot.ts +++ b/web/src/lib/api/knot.ts @@ -1,19 +1,19 @@ -import type { BobbinContext, QueryValue, XrpcRequestInit } from './client'; -import { jsonGet, rawGet } from './_request'; -import type * as Archive from './lexicons/types/sh/tangled/repo/archive'; -import type * as Blob_ from './lexicons/types/sh/tangled/repo/blob'; -import type * as Branch from './lexicons/types/sh/tangled/repo/branch'; -import type * as Branches from './lexicons/types/sh/tangled/repo/branches'; -import type * as Compare from './lexicons/types/sh/tangled/repo/compare'; -import type * as DescribeRepo from './lexicons/types/sh/tangled/repo/describeRepo'; -import type * as Diff from './lexicons/types/sh/tangled/repo/diff'; -import type * as GetDefaultBranch from './lexicons/types/sh/tangled/repo/getDefaultBranch'; -import type * as Languages from './lexicons/types/sh/tangled/repo/languages'; -import type * as ListSecrets from './lexicons/types/sh/tangled/repo/listSecrets'; -import type * as Log from './lexicons/types/sh/tangled/repo/log'; -import type * as Tag from './lexicons/types/sh/tangled/repo/tag'; -import type * as Tags from './lexicons/types/sh/tangled/repo/tags'; -import type * as Tree from './lexicons/types/sh/tangled/repo/tree'; +import type { BobbinContext, QueryValue, XrpcRequestInit } from "./client"; +import { jsonGet, rawGet } from "./_request"; +import type * as Archive from "./lexicons/types/sh/tangled/repo/archive"; +import type * as Blob_ from "./lexicons/types/sh/tangled/repo/blob"; +import type * as Branch from "./lexicons/types/sh/tangled/repo/branch"; +import type * as Branches from "./lexicons/types/sh/tangled/repo/branches"; +import type * as Compare from "./lexicons/types/sh/tangled/repo/compare"; +import type * as DescribeRepo from "./lexicons/types/sh/tangled/repo/describeRepo"; +import type * as Diff from "./lexicons/types/sh/tangled/repo/diff"; +import type * as GetDefaultBranch from "./lexicons/types/sh/tangled/repo/getDefaultBranch"; +import type * as Languages from "./lexicons/types/sh/tangled/repo/languages"; +import type * as ListSecrets from "./lexicons/types/sh/tangled/repo/listSecrets"; +import type * as Log from "./lexicons/types/sh/tangled/repo/log"; +import type * as Tag from "./lexicons/types/sh/tangled/repo/tag"; +import type * as Tags from "./lexicons/types/sh/tangled/repo/tags"; +import type * as Tree from "./lexicons/types/sh/tangled/repo/tree"; // wrappers for bobbin's knot-proxied repo endpoints. @@ -21,13 +21,13 @@ const asParams = (params: object): Record => params as unknown as Record; export const tree = (ctx: BobbinContext, params: Tree.$params, init?: XrpcRequestInit) => - jsonGet(ctx, 'sh.tangled.repo.tree', asParams(params), init); + jsonGet(ctx, "sh.tangled.repo.tree", asParams(params), init); export const blob = (ctx: BobbinContext, params: Blob_.$params, init?: XrpcRequestInit) => - jsonGet(ctx, 'sh.tangled.repo.blob', asParams(params), init); + jsonGet(ctx, "sh.tangled.repo.blob", asParams(params), init); export const branch = (ctx: BobbinContext, params: Branch.$params, init?: XrpcRequestInit) => - jsonGet(ctx, 'sh.tangled.repo.branch', asParams(params), init); + jsonGet(ctx, "sh.tangled.repo.branch", asParams(params), init); export const getDefaultBranch = ( ctx: BobbinContext, @@ -36,7 +36,7 @@ export const getDefaultBranch = ( ) => jsonGet( ctx, - 'sh.tangled.repo.getDefaultBranch', + "sh.tangled.repo.getDefaultBranch", asParams(params), init ); @@ -45,50 +45,50 @@ export const describeRepo = ( ctx: BobbinContext, params: DescribeRepo.$params, init?: XrpcRequestInit -) => jsonGet(ctx, 'sh.tangled.repo.describeRepo', asParams(params), init); +) => jsonGet(ctx, "sh.tangled.repo.describeRepo", asParams(params), init); export const languages = (ctx: BobbinContext, params: Languages.$params, init?: XrpcRequestInit) => - jsonGet(ctx, 'sh.tangled.repo.languages', asParams(params), init); + jsonGet(ctx, "sh.tangled.repo.languages", asParams(params), init); export const listSecrets = ( ctx: BobbinContext, params: ListSecrets.$params, init?: XrpcRequestInit -) => jsonGet(ctx, 'sh.tangled.repo.listSecrets', asParams(params), init); +) => jsonGet(ctx, "sh.tangled.repo.listSecrets", asParams(params), init); // schema-less json passthroughs; callers supply the shape. export const log = (ctx: BobbinContext, params: Log.$params, init?: XrpcRequestInit) => - jsonGet(ctx, 'sh.tangled.repo.log', asParams(params), init); + jsonGet(ctx, "sh.tangled.repo.log", asParams(params), init); export const diff = ( ctx: BobbinContext, params: Diff.$params, init?: XrpcRequestInit -) => jsonGet(ctx, 'sh.tangled.repo.diff', asParams(params), init); +) => jsonGet(ctx, "sh.tangled.repo.diff", asParams(params), init); export const compare = ( ctx: BobbinContext, params: Compare.$params, init?: XrpcRequestInit -) => jsonGet(ctx, 'sh.tangled.repo.compare', asParams(params), init); +) => jsonGet(ctx, "sh.tangled.repo.compare", asParams(params), init); export const branches = ( ctx: BobbinContext, params: Branches.$params, init?: XrpcRequestInit -) => jsonGet(ctx, 'sh.tangled.repo.branches', asParams(params), init); +) => jsonGet(ctx, "sh.tangled.repo.branches", asParams(params), init); export const tags = ( ctx: BobbinContext, params: Tags.$params, init?: XrpcRequestInit -) => jsonGet(ctx, 'sh.tangled.repo.tags', asParams(params), init); +) => jsonGet(ctx, "sh.tangled.repo.tags", asParams(params), init); export const tag = (ctx: BobbinContext, params: Tag.$params, init?: XrpcRequestInit) => - jsonGet(ctx, 'sh.tangled.repo.tag', asParams(params), init); + jsonGet(ctx, "sh.tangled.repo.tag", asParams(params), init); // raw response for streaming/download. export const archive = (ctx: BobbinContext, params: Archive.$params, init?: XrpcRequestInit) => - rawGet(ctx, 'sh.tangled.repo.archive', asParams(params), init); + rawGet(ctx, "sh.tangled.repo.archive", asParams(params), init); diff --git a/web/src/lib/api/load.test.ts b/web/src/lib/api/load.test.ts index d7f0ef88..3ea5dafc 100644 --- a/web/src/lib/api/load.test.ts +++ b/web/src/lib/api/load.test.ts @@ -1,121 +1,121 @@ -import { describe, expect, it, vi } from 'vitest'; -import { ClientResponseError } from './client'; -import { createRequestCache, httpStatusFor, parallel, toHttpError } from './load'; +import { describe, expect, it, vi } from "vitest"; +import { ClientResponseError } from "./client"; +import { createRequestCache, httpStatusFor, parallel, toHttpError } from "./load"; const cre = (status: number, error: string, message?: string): ClientResponseError => new ClientResponseError({ status, data: { error, message } }); -describe('httpStatusFor', () => { - it('passes through an in-band XRPC status', () => { - expect(httpStatusFor(cre(404, 'RecordNotFound'))).toBe(404); +describe("httpStatusFor", () => { + it("passes through an in-band XRPC status", () => { + expect(httpStatusFor(cre(404, "RecordNotFound"))).toBe(404); }); - it('prefers an in-band status over the error-name mapping', () => { + it("prefers an in-band status over the error-name mapping", () => { // status 503 is in [400,599] so it wins even though the name maps to 404. - expect(httpStatusFor(cre(503, 'RecordNotFound'))).toBe(503); + expect(httpStatusFor(cre(503, "RecordNotFound"))).toBe(503); }); - it('maps the error name when the status is out of the 400-599 band', () => { - expect(httpStatusFor(cre(200, 'RecordNotFound'))).toBe(404); - expect(httpStatusFor(cre(200, 'InvalidRequest'))).toBe(400); - expect(httpStatusFor(cre(200, 'UpstreamFailed'))).toBe(502); - expect(httpStatusFor(cre(200, 'UpstreamGone'))).toBe(502); - expect(httpStatusFor(cre(200, 'InvalidRecord'))).toBe(502); - expect(httpStatusFor(cre(200, 'Overloaded'))).toBe(503); - expect(httpStatusFor(cre(200, 'SomethingElse'))).toBe(500); + it("maps the error name when the status is out of the 400-599 band", () => { + expect(httpStatusFor(cre(200, "RecordNotFound"))).toBe(404); + expect(httpStatusFor(cre(200, "InvalidRequest"))).toBe(400); + expect(httpStatusFor(cre(200, "UpstreamFailed"))).toBe(502); + expect(httpStatusFor(cre(200, "UpstreamGone"))).toBe(502); + expect(httpStatusFor(cre(200, "InvalidRecord"))).toBe(502); + expect(httpStatusFor(cre(200, "Overloaded"))).toBe(503); + expect(httpStatusFor(cre(200, "SomethingElse"))).toBe(500); }); - it('maps any non-XRPC cause to 500', () => { - expect(httpStatusFor(new Error('boom'))).toBe(500); - expect(httpStatusFor('a string')).toBe(500); + it("maps any non-XRPC cause to 500", () => { + expect(httpStatusFor(new Error("boom"))).toBe(500); + expect(httpStatusFor("a string")).toBe(500); expect(httpStatusFor(undefined)).toBe(500); expect(httpStatusFor({ status: 404 })).toBe(500); }); }); -describe('toHttpError', () => { - it('throws a SvelteKit error with the mapped status and the XRPC description', () => { +describe("toHttpError", () => { + it("throws a SvelteKit error with the mapped status and the XRPC description", () => { let thrown: unknown; try { - toHttpError(cre(404, 'RecordNotFound', 'no such repo')); + toHttpError(cre(404, "RecordNotFound", "no such repo")); } catch (e) { thrown = e; } - expect(thrown).toMatchObject({ status: 404, body: { message: 'no such repo' } }); + expect(thrown).toMatchObject({ status: 404, body: { message: "no such repo" } }); }); - it('falls back to the error name when the description is absent', () => { + it("falls back to the error name when the description is absent", () => { let thrown: unknown; try { - toHttpError(cre(200, 'Overloaded')); + toHttpError(cre(200, "Overloaded")); } catch (e) { thrown = e; } - expect(thrown).toMatchObject({ status: 503, body: { message: 'Overloaded' } }); + expect(thrown).toMatchObject({ status: 503, body: { message: "Overloaded" } }); }); - it('uses the fallback message + 500 for a non-XRPC cause', () => { + it("uses the fallback message + 500 for a non-XRPC cause", () => { let thrown: unknown; try { - toHttpError(new Error('boom'), 'could not load'); + toHttpError(new Error("boom"), "could not load"); } catch (e) { thrown = e; } - expect(thrown).toMatchObject({ status: 500, body: { message: 'could not load' } }); + expect(thrown).toMatchObject({ status: 500, body: { message: "could not load" } }); }); }); -describe('parallel', () => { - it('preserves key/value pairing regardless of settle order', async () => { +describe("parallel", () => { + it("preserves key/value pairing regardless of settle order", async () => { // deferred across a few microtasks (no real timer) so it settles after `fast`. const slow = Promise.resolve() .then(() => Promise.resolve()) - .then(() => 'slow'); - const result = await parallel({ fast: Promise.resolve('fast'), slow }); - expect(result).toEqual({ fast: 'fast', slow: 'slow' }); + .then(() => "slow"); + const result = await parallel({ fast: Promise.resolve("fast"), slow }); + expect(result).toEqual({ fast: "fast", slow: "slow" }); }); - it('resolves to an empty object for no tasks', async () => { + it("resolves to an empty object for no tasks", async () => { await expect(parallel({})).resolves.toEqual({}); }); - it('rejects if any input promise rejects', async () => { + it("rejects if any input promise rejects", async () => { await expect( - parallel({ a: Promise.resolve(1), b: Promise.reject(new Error('boom')) }) - ).rejects.toThrow('boom'); + parallel({ a: Promise.resolve(1), b: Promise.reject(new Error("boom")) }) + ).rejects.toThrow("boom"); }); }); -describe('createRequestCache', () => { - it('invokes the loader once per key and shares the value', async () => { +describe("createRequestCache", () => { + it("invokes the loader once per key and shares the value", async () => { const cache = createRequestCache(); const load = vi.fn(async () => 42); - const first = await cache.run('k', load); - const second = await cache.run('k', load); + const first = await cache.run("k", load); + const second = await cache.run("k", load); expect(first).toBe(42); expect(second).toBe(42); expect(load).toHaveBeenCalledTimes(1); }); - it('shares one in-flight promise for concurrent same-key calls', () => { + it("shares one in-flight promise for concurrent same-key calls", () => { const cache = createRequestCache(); - const load = vi.fn(async () => 'v'); + const load = vi.fn(async () => "v"); - const a = cache.run('k', load); - const b = cache.run('k', load); + const a = cache.run("k", load); + const b = cache.run("k", load); expect(a).toBe(b); expect(load).toHaveBeenCalledTimes(1); }); - it('runs distinct keys independently', async () => { + it("runs distinct keys independently", async () => { const cache = createRequestCache(); const load = vi.fn(async (key: string) => key.toUpperCase()); - await cache.run('a', () => load('a')); - await cache.run('b', () => load('b')); + await cache.run("a", () => load("a")); + await cache.run("b", () => load("b")); expect(load).toHaveBeenCalledTimes(2); }); diff --git a/web/src/lib/api/load.ts b/web/src/lib/api/load.ts index 543fb1b7..7e9bd850 100644 --- a/web/src/lib/api/load.ts +++ b/web/src/lib/api/load.ts @@ -1,19 +1,19 @@ -import { error, type NumericRange } from '@sveltejs/kit'; -import { ClientResponseError } from './client'; +import { error, type NumericRange } from "@sveltejs/kit"; +import { ClientResponseError } from "./client"; export const httpStatusFor = (cause: unknown): number => { if (cause instanceof ClientResponseError) { if (cause.status >= 400 && cause.status <= 599) return cause.status; switch (cause.error) { - case 'RecordNotFound': + case "RecordNotFound": return 404; - case 'InvalidRequest': + case "InvalidRequest": return 400; - case 'UpstreamFailed': - case 'UpstreamGone': - case 'InvalidRecord': + case "UpstreamFailed": + case "UpstreamGone": + case "InvalidRecord": return 502; - case 'Overloaded': + case "Overloaded": return 503; default: return 500; @@ -22,7 +22,7 @@ export const httpStatusFor = (cause: unknown): number => { return 500; }; -export const toHttpError = (cause: unknown, fallbackMessage = 'Request failed'): never => { +export const toHttpError = (cause: unknown, fallbackMessage = "Request failed"): never => { const status = httpStatusFor(cause) as NumericRange<400, 599>; const message = cause instanceof ClientResponseError ? (cause.description ?? cause.error) : fallbackMessage; diff --git a/web/src/lib/api/pagination.test.ts b/web/src/lib/api/pagination.test.ts index 5bebb92e..923d6186 100644 --- a/web/src/lib/api/pagination.test.ts +++ b/web/src/lib/api/pagination.test.ts @@ -1,9 +1,9 @@ -import { describe, expect, it, vi, type Mock } from 'vitest'; -import { createBobbinClient, type BobbinContext } from './client'; -import { collect, pages, paginateBy } from './pagination'; +import { describe, expect, it, vi, type Mock } from "vitest"; +import { createBobbinClient, type BobbinContext } from "./client"; +import { collect, pages, paginateBy } from "./pagination"; -const NAME = 'sh.tangled.feed.listStars'; -const PARAMS = { subject: 'did:plc:x' } as const; +const NAME = "sh.tangled.feed.listStars"; +const PARAMS = { subject: "did:plc:x" } as const; interface Page { items: readonly { uri: string }[]; @@ -13,59 +13,59 @@ interface Page { const pageResponse = (page: Page): Response => new Response(JSON.stringify(page), { status: 200, - headers: { 'content-type': 'application/json' } + headers: { "content-type": "application/json" } }); const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext => - createBobbinClient({ serviceUrl: 'https://bobbin.test', fetch: fetchMock }); + createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); const cursorOf = (mock: Mock, n: number): string | null => - new URL(String(mock.mock.calls[n][0])).searchParams.get('cursor'); + new URL(String(mock.mock.calls[n][0])).searchParams.get("cursor"); const threePageFetch = (): Mock => vi .fn() - .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'a' }, { uri: 'b' }], cursor: 'c1' })) - .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'c' }], cursor: 'c2' })) - .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'd' }] })); + .mockResolvedValueOnce(pageResponse({ items: [{ uri: "a" }, { uri: "b" }], cursor: "c1" })) + .mockResolvedValueOnce(pageResponse({ items: [{ uri: "c" }], cursor: "c2" })) + .mockResolvedValueOnce(pageResponse({ items: [{ uri: "d" }] })); -describe('pages / items / collect follow the cursor', () => { - it('pages() yields every page then stops when a page omits the cursor', async () => { +describe("pages / items / collect follow the cursor", () => { + it("pages() yields every page then stops when a page omits the cursor", async () => { const fetchMock = threePageFetch(); const seen: Page[] = []; for await (const p of pages(makeCtx(fetchMock), NAME, PARAMS)) seen.push(p as Page); expect(fetchMock).toHaveBeenCalledTimes(3); - expect(seen.map((p) => p.cursor)).toEqual(['c1', 'c2', undefined]); + expect(seen.map((p) => p.cursor)).toEqual(["c1", "c2", undefined]); expect(cursorOf(fetchMock, 0)).toBeNull(); - expect(cursorOf(fetchMock, 1)).toBe('c1'); - expect(cursorOf(fetchMock, 2)).toBe('c2'); + expect(cursorOf(fetchMock, 1)).toBe("c1"); + expect(cursorOf(fetchMock, 2)).toBe("c2"); }); }); -describe('pagination caps', () => { - it('maxPages caps the number of network round-trips even with unbounded cursors', async () => { +describe("pagination caps", () => { + it("maxPages caps the number of network round-trips even with unbounded cursors", async () => { const fetchMock = vi .fn() - .mockImplementation(async () => pageResponse({ items: [{ uri: 'x' }], cursor: 'always' })); + .mockImplementation(async () => pageResponse({ items: [{ uri: "x" }], cursor: "always" })); const out = await collect(makeCtx(fetchMock), NAME, PARAMS, { maxPages: 2 }); expect(fetchMock).toHaveBeenCalledTimes(2); expect(out).toHaveLength(2); }); - it('collect({ max }) caps items mid-page without fetching the next page', async () => { + it("collect({ max }) caps items mid-page without fetching the next page", async () => { const fetchMock = vi .fn() .mockResolvedValueOnce( - pageResponse({ items: [{ uri: 'a' }, { uri: 'b' }, { uri: 'c' }], cursor: 'c1' }) + pageResponse({ items: [{ uri: "a" }, { uri: "b" }, { uri: "c" }], cursor: "c1" }) ); const out = await collect(makeCtx(fetchMock), NAME, PARAMS, { max: 2 }); - expect(out).toEqual([{ uri: 'a' }, { uri: 'b' }]); + expect(out).toEqual([{ uri: "a" }, { uri: "b" }]); expect(fetchMock).toHaveBeenCalledTimes(1); }); }); -describe('paginateBy', () => { - it('treats cursor: null as the end of the stream', async () => { +describe("paginateBy", () => { + it("treats cursor: null as the end of the stream", async () => { const load = vi.fn(async () => ({ items: [1, 2] as const, cursor: null @@ -76,27 +76,27 @@ describe('paginateBy', () => { expect(load).toHaveBeenCalledTimes(1); }); - it('follows a string cursor and forwards it to the loader', async () => { + it("follows a string cursor and forwards it to the loader", async () => { const load = vi .fn< ( cursor: string | undefined ) => Promise<{ items: readonly number[]; cursor?: string | null }> >() - .mockResolvedValueOnce({ items: [1], cursor: 'p2' }) + .mockResolvedValueOnce({ items: [1], cursor: "p2" }) .mockResolvedValueOnce({ items: [2], cursor: undefined }); const out: number[] = []; for await (const n of paginateBy(load)) out.push(n); expect(out).toEqual([1, 2]); expect(load).toHaveBeenCalledTimes(2); expect(load.mock.calls[0][0]).toBeUndefined(); - expect(load.mock.calls[1][0]).toBe('p2'); + expect(load.mock.calls[1][0]).toBe("p2"); }); - it('maxPages caps loader invocations', async () => { + it("maxPages caps loader invocations", async () => { const load = vi.fn(async () => ({ items: [0], - cursor: 'always' + cursor: "always" })); const out: number[] = []; for await (const n of paginateBy(load, { maxPages: 3 })) out.push(n); diff --git a/web/src/lib/api/pagination.ts b/web/src/lib/api/pagination.ts index a4d6fcb3..d873d0b2 100644 --- a/web/src/lib/api/pagination.ts +++ b/web/src/lib/api/pagination.ts @@ -1,20 +1,20 @@ -import type { XRPCQueries } from '@atcute/lexicons/ambient'; +import type { XRPCQueries } from "@atcute/lexicons/ambient"; import type { InferInput, InferOutput, ObjectSchema, XRPCLexBodyParam -} from '@atcute/lexicons/validations'; -import { ok, type BobbinContext, type XrpcRequestInit } from './client'; +} from "@atcute/lexicons/validations"; +import { ok, type BobbinContext, type XrpcRequestInit } from "./client"; export type QueryOutput = - XRPCQueries[TName]['output'] extends XRPCLexBodyParam - ? InferOutput + XRPCQueries[TName]["output"] extends XRPCLexBodyParam + ? InferOutput : never; export type QueryParams = - XRPCQueries[TName]['params'] extends ObjectSchema - ? InferInput + XRPCQueries[TName]["params"] extends ObjectSchema + ? InferInput : Record; type CursorPage = { items: readonly unknown[]; cursor?: string }; @@ -27,7 +27,7 @@ export type PaginatedQuery = { export type PageItem = QueryOutput extends { items: readonly (infer I)[] } ? I : never; -export type PageParams = Omit, 'cursor'>; +export type PageParams = Omit, "cursor">; export interface PaginateOptions extends XrpcRequestInit { /** stop after this many network round-trips. */ diff --git a/web/src/lib/api/profile.ts b/web/src/lib/api/profile.ts index ab55cf2d..e52eb816 100644 --- a/web/src/lib/api/profile.ts +++ b/web/src/lib/api/profile.ts @@ -1,11 +1,11 @@ -import { ok } from '@atcute/client'; -import { mainSchema as putRecordSchema } from '@atcute/atproto/types/repo/putRecord'; -import type { Nsid, RecordKey } from '@atcute/lexicons/syntax'; -import type { OAuthUserAgent } from '@atcute/oauth-browser-client'; -import { createClient } from '$lib/auth/agent'; -import type { ProfileRecord } from './records'; +import { ok } from "@atcute/client"; +import { mainSchema as putRecordSchema } from "@atcute/atproto/types/repo/putRecord"; +import type { Nsid, RecordKey } from "@atcute/lexicons/syntax"; +import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; +import { createClient } from "$lib/auth/agent"; +import type { ProfileRecord } from "./records"; -const PROFILE_COLLECTION = 'sh.tangled.actor.profile' as Nsid; +const PROFILE_COLLECTION = "sh.tangled.actor.profile" as Nsid; // the profile record lives at the fixed rkey `self`; put upserts it. export const putProfile = async (agent: OAuthUserAgent, record: ProfileRecord): Promise => { @@ -15,7 +15,7 @@ export const putProfile = async (agent: OAuthUserAgent, record: ProfileRecord): input: { repo: agent.sub, collection: PROFILE_COLLECTION, - rkey: 'self' as RecordKey, + rkey: "self" as RecordKey, record } }) diff --git a/web/src/lib/api/records.ts b/web/src/lib/api/records.ts index 9bcfebff..bd450e04 100644 --- a/web/src/lib/api/records.ts +++ b/web/src/lib/api/records.ts @@ -1,9 +1,9 @@ -import type { BobbinContext, XrpcRequestInit } from './client'; -import { jsonGet } from './_request'; -import type * as ShTangledActorProfile from './lexicons/types/sh/tangled/actor/profile'; -import type * as ShTangledRepo from './lexicons/types/sh/tangled/repo'; -import type * as ShTangledRepoIssue from './lexicons/types/sh/tangled/repo/issue'; -import type * as ShTangledRepoPull from './lexicons/types/sh/tangled/repo/pull'; +import type { BobbinContext, XrpcRequestInit } from "./client"; +import { jsonGet } from "./_request"; +import type * as ShTangledActorProfile from "./lexicons/types/sh/tangled/actor/profile"; +import type * as ShTangledRepo from "./lexicons/types/sh/tangled/repo"; +import type * as ShTangledRepoIssue from "./lexicons/types/sh/tangled/repo/issue"; +import type * as ShTangledRepoPull from "./lexicons/types/sh/tangled/repo/pull"; export interface RecordView { uri: string; @@ -23,36 +23,36 @@ export type PullRecord = ShTangledRepoPull.Main; export const BULK_LIMIT = 50; export const getRepo = (ctx: BobbinContext, repo: string, init?: XrpcRequestInit) => - jsonGet>(ctx, 'sh.tangled.repo.getRepo', { repo }, init); + jsonGet>(ctx, "sh.tangled.repo.getRepo", { repo }, init); export const getRepoByRepoDid = (ctx: BobbinContext, repoDid: string, init?: XrpcRequestInit) => - jsonGet>(ctx, 'sh.tangled.repo.getRepoByRepoDid', { repoDid }, init); + jsonGet>(ctx, "sh.tangled.repo.getRepoByRepoDid", { repoDid }, init); export const getProfile = (ctx: BobbinContext, did: string, init?: XrpcRequestInit) => jsonGet>( ctx, - 'sh.tangled.actor.getProfile', + "sh.tangled.actor.getProfile", { actor: `at://${did}/sh.tangled.actor.profile/self` }, init ); export const getIssue = (ctx: BobbinContext, issue: string, init?: XrpcRequestInit) => - jsonGet>(ctx, 'sh.tangled.repo.getIssue', { issue }, init); + jsonGet>(ctx, "sh.tangled.repo.getIssue", { issue }, init); export const getPull = (ctx: BobbinContext, pull: string, init?: XrpcRequestInit) => - jsonGet>(ctx, 'sh.tangled.repo.getPull', { pull }, init); + jsonGet>(ctx, "sh.tangled.repo.getPull", { pull }, init); export const getRepos = (ctx: BobbinContext, repos: readonly string[], init?: XrpcRequestInit) => - jsonGet>(ctx, 'sh.tangled.repo.getRepos', { repos }, init); + jsonGet>(ctx, "sh.tangled.repo.getRepos", { repos }, init); export const getProfiles = ( ctx: BobbinContext, actors: readonly string[], init?: XrpcRequestInit -) => jsonGet>(ctx, 'sh.tangled.actor.getProfiles', { actors }, init); +) => jsonGet>(ctx, "sh.tangled.actor.getProfiles", { actors }, init); export const getIssues = (ctx: BobbinContext, issues: readonly string[], init?: XrpcRequestInit) => - jsonGet>(ctx, 'sh.tangled.repo.getIssues', { issues }, init); + jsonGet>(ctx, "sh.tangled.repo.getIssues", { issues }, init); export const getPulls = (ctx: BobbinContext, pulls: readonly string[], init?: XrpcRequestInit) => - jsonGet>(ctx, 'sh.tangled.repo.getPulls', { pulls }, init); + jsonGet>(ctx, "sh.tangled.repo.getPulls", { pulls }, init); diff --git a/web/src/lib/api/search.ts b/web/src/lib/api/search.ts index a55a1f3e..f95f0bcc 100644 --- a/web/src/lib/api/search.ts +++ b/web/src/lib/api/search.ts @@ -1,6 +1,6 @@ -import type { BobbinContext, XrpcRequestInit } from './client'; -import { jsonGet } from './_request'; -import { paginateBy } from './pagination'; +import type { BobbinContext, XrpcRequestInit } from "./client"; +import { jsonGet } from "./_request"; +import { paginateBy } from "./pagination"; export interface SearchHit { uri: string; @@ -28,7 +28,7 @@ export interface SearchParams { } export const search = (ctx: BobbinContext, params: SearchParams, init?: XrpcRequestInit) => - jsonGet(ctx, 'sh.tangled.search.query', { ...params }, init); + jsonGet(ctx, "sh.tangled.search.query", { ...params }, init); export interface SearchAllOptions extends XrpcRequestInit { maxPages?: number; @@ -37,7 +37,7 @@ export interface SearchAllOptions extends XrpcRequestInit { /** Async-iterate every search hit across cursor pages. */ export async function* searchAll( ctx: BobbinContext, - params: Omit, + params: Omit, options: SearchAllOptions = {} ): AsyncGenerator { yield* paginateBy(async (cursor) => { diff --git a/web/src/lib/api/uri.ts b/web/src/lib/api/uri.ts index 8fdaa654..bbd09787 100644 --- a/web/src/lib/api/uri.ts +++ b/web/src/lib/api/uri.ts @@ -1,9 +1,9 @@ // at-uri helpers: at://// export const didFromUri = (uri: string): string => { - const rest = uri.startsWith('at://') ? uri.slice(5) : uri; - const slash = rest.indexOf('/'); + const rest = uri.startsWith("at://") ? uri.slice(5) : uri; + const slash = rest.indexOf("/"); return slash === -1 ? rest : rest.slice(0, slash); }; -export const rkeyFromUri = (uri: string): string => uri.slice(uri.lastIndexOf('/') + 1); +export const rkeyFromUri = (uri: string): string => uri.slice(uri.lastIndexOf("/") + 1); diff --git a/web/src/lib/auth.svelte.ts b/web/src/lib/auth.svelte.ts index 99a0056e..c2be4ef9 100644 --- a/web/src/lib/auth.svelte.ts +++ b/web/src/lib/auth.svelte.ts @@ -1,12 +1,12 @@ -import { browser } from '$app/environment'; +import { browser } from "$app/environment"; import { CompositeDidDocumentResolver, LocalActorResolver, PlcDidDocumentResolver, WebDidDocumentResolver, XrpcHandleResolver -} from '@atcute/identity-resolver'; -import type { ActorIdentifier, Did } from '@atcute/lexicons/syntax'; +} from "@atcute/identity-resolver"; +import type { ActorIdentifier, Did } from "@atcute/lexicons/syntax"; import { OAuthUserAgent, configureOAuth, @@ -15,10 +15,10 @@ import { finalizeAuthorization, getSession, listStoredSessions -} from '@atcute/oauth-browser-client'; -import { getContext } from 'svelte'; -import { SvelteURL, SvelteURLSearchParams } from 'svelte/reactivity'; -import oauthMetadata from '../../static/oauth-client-metadata.json'; +} from "@atcute/oauth-browser-client"; +import { getContext } from "svelte"; +import { SvelteURL, SvelteURLSearchParams } from "svelte/reactivity"; +import oauthMetadata from "../../static/oauth-client-metadata.json"; import { type AuthAccount, clearActive, @@ -29,19 +29,19 @@ import { reconcileAccounts, saveAccounts, upsertAccount -} from './auth/accounts'; +} from "./auth/accounts"; -export const AUTH_KEY = Symbol('auth'); -const DEV_REDIRECT_URI = 'http://127.0.0.1:5173/oauth/callback'; +export const AUTH_KEY = Symbol("auth"); +const DEV_REDIRECT_URI = "http://127.0.0.1:5173/oauth/callback"; const DEV_CLIENT_ID = `http://localhost?redirect_uri=${encodeURIComponent(DEV_REDIRECT_URI)}&scope=${encodeURIComponent(oauthMetadata.scope)}`; // local dev (localinfra) points these at the local pds/plc; defaults are the public network. const HANDLE_RESOLVER_URL = - (import.meta.env.VITE_HANDLE_RESOLVER_URL as string | undefined)?.replace(/\/+$/, '') ?? - 'https://public.api.bsky.app'; + (import.meta.env.VITE_HANDLE_RESOLVER_URL as string | undefined)?.replace(/\/+$/, "") ?? + "https://public.api.bsky.app"; const PLC_DIRECTORY_URL = (import.meta.env.VITE_PLC_DIRECTORY_URL as string | undefined)?.replace( /\/+$/, - '' + "" ); const identityResolver = new LocalActorResolver({ @@ -68,7 +68,7 @@ export interface CurrentUser { avatar?: string; } -export type { AuthAccount } from './auth/accounts'; +export type { AuthAccount } from "./auth/accounts"; export interface Auth { readonly agent: OAuthUserAgent | null; @@ -100,7 +100,7 @@ type MiniDoc = { type OAuthSession = ConstructorParameters[0]; const ENV_OAUTH_REDIRECT_URI = import.meta.env.VITE_OAUTH_REDIRECT_URI as string | undefined; -const HAS_LOCALHOST_REDIRECT = ENV_OAUTH_REDIRECT_URI?.includes('://localhost') ?? false; +const HAS_LOCALHOST_REDIRECT = ENV_OAUTH_REDIRECT_URI?.includes("://localhost") ?? false; const OAUTH_CLIENT_ID = HAS_LOCALHOST_REDIRECT ? DEV_CLIENT_ID : ((import.meta.env.VITE_OAUTH_CLIENT_ID as string | undefined) ?? DEV_CLIENT_ID); @@ -125,8 +125,8 @@ const configure = () => { const errorMessage = (cause: unknown) => { const message = cause instanceof Error ? cause.message : String(cause); - return message.toLowerCase().includes('unknown state') - ? 'Could not resume OAuth state. In local development, start login from http://127.0.0.1:5173 instead of localhost.' + return message.toLowerCase().includes("unknown state") + ? "Could not resume OAuth state. In local development, start login from http://127.0.0.1:5173 instead of localhost." : message; }; @@ -135,9 +135,9 @@ const resolveProfile = async ( bobbinUrl: string ): Promise => { try { - const url = new URL('/xrpc/com.bad-example.identity.resolveMiniDoc', bobbinUrl); - url.searchParams.set('identifier', identifier); - const response = await fetch(url, { headers: { accept: 'application/json' } }); + const url = new URL("/xrpc/com.bad-example.identity.resolveMiniDoc", bobbinUrl); + url.searchParams.set("identifier", identifier); + const response = await fetch(url, { headers: { accept: "application/json" } }); if (response.ok) { const profile = (await response.json()) as MiniDoc; return { @@ -162,11 +162,11 @@ const resolveProfile = async ( }; const returnToFromState = (state: object | null): string => { - if (state && typeof state === 'object' && 'returnTo' in state) { + if (state && typeof state === "object" && "returnTo" in state) { const returnTo = state.returnTo; - if (typeof returnTo === 'string') return returnTo; + if (typeof returnTo === "string") return returnTo; } - return '/'; + return "/"; }; export const createAuth = ( @@ -255,22 +255,22 @@ export const createAuth = ( resetLoggedOut(); }; - const signIn = async (identifier: string, returnTo = '/') => { + const signIn = async (identifier: string, returnTo = "/") => { if (!browser) return; configure(); error = null; const trimmed = identifier.trim(); if (!trimmed) { - error = 'Handle or DID required'; + error = "Handle or DID required"; return; } - if (location.hostname === 'localhost') { + if (location.hostname === "localhost") { const url = new SvelteURL(location.href); - url.hostname = '127.0.0.1'; - url.searchParams.set('identifier', trimmed); - url.searchParams.set('return_url', returnTo); + url.hostname = "127.0.0.1"; + url.searchParams.set("identifier", trimmed); + url.searchParams.set("return_url", returnTo); location.replace(url); return; } @@ -278,7 +278,7 @@ export const createAuth = ( try { const url = await createAuthorizationUrl({ target: { - type: 'account', + type: "account", identifier: trimmed as ActorIdentifier }, scope: OAUTH_SCOPE, @@ -293,14 +293,14 @@ export const createAuth = ( }; const completeSignIn = async () => { - if (!browser) return '/'; + if (!browser) return "/"; configure(); error = null; authenticating = true; try { const params = new SvelteURLSearchParams(location.hash.slice(1)); - history.replaceState(null, '', location.pathname + location.search); + history.replaceState(null, "", location.pathname + location.search); const { session, state } = await finalizeAuthorization(params); adoptSession(session); diff --git a/web/src/lib/auth/accounts.ts b/web/src/lib/auth/accounts.ts index 6cdcbd74..1bb32723 100644 --- a/web/src/lib/auth/accounts.ts +++ b/web/src/lib/auth/accounts.ts @@ -1,11 +1,11 @@ -import { browser } from '$app/environment'; -import type { Did } from '@atcute/lexicons/syntax'; +import { browser } from "$app/environment"; +import type { Did } from "@atcute/lexicons/syntax"; // atcute owns oauth sessions; this stores metadata/order and active-account cookies. -export const CURRENT_DID_KEY = 'tangled.currentDid'; -export const CURRENT_HANDLE_KEY = 'tangled.currentHandle'; -const ACCOUNTS_KEY = 'tangled.accounts'; +export const CURRENT_DID_KEY = "tangled.currentDid"; +export const CURRENT_HANDLE_KEY = "tangled.currentHandle"; +const ACCOUNTS_KEY = "tangled.accounts"; // appview account cap parity export const MAX_ACCOUNTS = 20; @@ -19,13 +19,13 @@ export interface AuthAccount { } const isDid = (value: unknown): value is Did => - typeof value === 'string' && value.startsWith('did:'); + typeof value === "string" && value.startsWith("did:"); const isAccount = (value: unknown): value is AuthAccount => !!value && - typeof value === 'object' && + typeof value === "object" && isDid((value as AuthAccount).did) && - typeof (value as AuthAccount).handle === 'string'; + typeof (value as AuthAccount).handle === "string"; export const loadAccounts = (): AuthAccount[] => { if (!browser) return []; @@ -38,7 +38,7 @@ export const loadAccounts = (): AuthAccount[] => { did: account.did, handle: account.handle, avatar: account.avatar, - addedAt: typeof account.addedAt === 'number' ? account.addedAt : 0 + addedAt: typeof account.addedAt === "number" ? account.addedAt : 0 })); } catch { return []; @@ -88,7 +88,7 @@ export const persistActive = (did: string, handle: string): void => { if (!browser) return; localStorage.setItem(CURRENT_DID_KEY, did); localStorage.setItem(CURRENT_HANDLE_KEY, handle); - const secure = location.protocol === 'https:' ? '; secure' : ''; + const secure = location.protocol === "https:" ? "; secure" : ""; const attrs = `; path=/; max-age=31536000; samesite=lax${secure}`; document.cookie = `${CURRENT_DID_KEY}=${encodeURIComponent(did)}${attrs}`; document.cookie = `${CURRENT_HANDLE_KEY}=${encodeURIComponent(handle)}${attrs}`; diff --git a/web/src/lib/auth/agent.ts b/web/src/lib/auth/agent.ts index a7065fbf..93311d1c 100644 --- a/web/src/lib/auth/agent.ts +++ b/web/src/lib/auth/agent.ts @@ -1,7 +1,7 @@ -import { Client, ok } from '@atcute/client'; -import { mainSchema as getServiceAuthSchema } from '@atcute/atproto/types/server/getServiceAuth'; -import type { Nsid } from '@atcute/lexicons/syntax'; -import type { OAuthUserAgent } from '@atcute/oauth-browser-client'; +import { Client, ok } from "@atcute/client"; +import { mainSchema as getServiceAuthSchema } from "@atcute/atproto/types/server/getServiceAuth"; +import type { Nsid } from "@atcute/lexicons/syntax"; +import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; export const createClient = (agent: OAuthUserAgent): Client => new Client({ handler: agent }); @@ -14,7 +14,7 @@ export interface ServiceAuthOptions { } // did:web service id, with ports percent-encoded like serviceauth didweb. -export const serviceDidForHost = (host: string): string => `did:web:${host.replace(/:/g, '%3A')}`; +export const serviceDidForHost = (host: string): string => `did:web:${host.replace(/:/g, "%3A")}`; // mint a service-auth jwt for knot/spindle xrpc calls. export const mintServiceAuth = async ( diff --git a/web/src/lib/auth/guards.ts b/web/src/lib/auth/guards.ts index 95cda3fd..bc6b899d 100644 --- a/web/src/lib/auth/guards.ts +++ b/web/src/lib/auth/guards.ts @@ -1,8 +1,8 @@ -import { goto } from '$app/navigation'; -import { resolve } from '$app/paths'; -import { redirect, type RequestEvent } from '@sveltejs/kit'; -import { CURRENT_DID_KEY, CURRENT_HANDLE_KEY } from './accounts'; -import type { Auth } from '../auth.svelte'; +import { goto } from "$app/navigation"; +import { resolve } from "$app/paths"; +import { redirect, type RequestEvent } from "@sveltejs/kit"; +import { CURRENT_DID_KEY, CURRENT_HANDLE_KEY } from "./accounts"; +import type { Auth } from "../auth.svelte"; export interface RequireAuthResult { did: string; @@ -22,7 +22,7 @@ export const requireAuth = (event: RequestEvent): RequireAuthResult => { export const requireAuthClient = (auth: Auth, url: URL): boolean => { if (auth.currentDid) return true; - void goto(resolve(loginWithReturn(url.pathname + url.search) as '/login'), { + void goto(resolve(loginWithReturn(url.pathname + url.search) as "/login"), { replaceState: true }); return false; diff --git a/web/src/lib/components/profile/FollowerFollowing.svelte b/web/src/lib/components/profile/FollowerFollowing.svelte index 638a7121..9c959f73 100644 --- a/web/src/lib/components/profile/FollowerFollowing.svelte +++ b/web/src/lib/components/profile/FollowerFollowing.svelte @@ -1,6 +1,6 @@ diff --git a/web/src/lib/components/profile/tabs/VouchTab.svelte b/web/src/lib/components/profile/tabs/VouchTab.svelte index c24051d2..83e2f68e 100644 --- a/web/src/lib/components/profile/tabs/VouchTab.svelte +++ b/web/src/lib/components/profile/tabs/VouchTab.svelte @@ -1,7 +1,7 @@ diff --git a/web/src/lib/components/profile/types.ts b/web/src/lib/components/profile/types.ts index 4be138e5..cb3de0d4 100644 --- a/web/src/lib/components/profile/types.ts +++ b/web/src/lib/components/profile/types.ts @@ -58,11 +58,11 @@ export interface VouchData { did: string; handle: string; avatar?: string; - kind: 'vouch' | 'denounce'; + kind: "vouch" | "denounce"; reason?: string; createdAt: string; } export type StarData = - | { kind: 'repo'; uri: string; createdAt: string; repo: RepoCardData } - | { kind: 'string'; uri: string; createdAt: string; ownerHandle: string; rkey: string }; + | { kind: "repo"; uri: string; createdAt: string; repo: RepoCardData } + | { kind: "string"; uri: string; createdAt: string; ownerHandle: string; rkey: string }; diff --git a/web/src/lib/components/ui/Spinner.svelte b/web/src/lib/components/ui/Spinner.svelte index 6581b54b..bc59160a 100644 --- a/web/src/lib/components/ui/Spinner.svelte +++ b/web/src/lib/components/ui/Spinner.svelte @@ -1,11 +1,11 @@