···11-import type { Handle } from '@sveltejs/kit';
11+import type { Handle } from "@sveltejs/kit";
2233export const handle: Handle = async ({ event, resolve }) => {
44 return resolve(event, {
···99 // the data we already had.
1010 // so we allow these headers to have atcute function properly.
1111 filterSerializedResponseHeaders(name) {
1212- return name === 'content-type' || name === 'content-length';
1212+ return name === "content-type" || name === "content-length";
1313 }
1414 });
1515};
+4-4
web/src/icon.d.ts
···11// `$icon/<name>` is a vite alias for unplugin-icons' `~icons/lucide/<name>`
22// virtual module (see vite.config.ts). mirror its ambient type here so tsc /
33// svelte-check resolve the aliased imports. must stay a global .d.ts (no export)
44-declare module '$icon/*' {
55- import type { Component } from 'svelte';
66- import type { SvelteHTMLElements } from 'svelte/elements';
44+declare module "$icon/*" {
55+ import type { Component } from "svelte";
66+ import type { SvelteHTMLElements } from "svelte/elements";
7788- const component: Component<SvelteHTMLElements['svg']>;
88+ const component: Component<SvelteHTMLElements["svg"]>;
991010 export default component;
1111}
···11-export * from './client';
22-export * from './pagination';
33-export * from './count';
44-export * from './records';
55-export * as knot from './knot';
66-export * from './search';
77-export * from './coverage';
88-export * from './identity';
99-export * from './load';
1010-export * from './uri';
1111-export * from './graph';
11+export * from "./client";
22+export * from "./pagination";
33+export * from "./count";
44+export * from "./records";
55+export * as knot from "./knot";
66+export * from "./search";
77+export * from "./coverage";
88+export * from "./identity";
99+export * from "./load";
1010+export * from "./uri";
1111+export * from "./graph";
···11-import type { BobbinContext, QueryValue, XrpcRequestInit } from './client';
22-import { jsonGet, rawGet } from './_request';
33-import type * as Archive from './lexicons/types/sh/tangled/repo/archive';
44-import type * as Blob_ from './lexicons/types/sh/tangled/repo/blob';
55-import type * as Branch from './lexicons/types/sh/tangled/repo/branch';
66-import type * as Branches from './lexicons/types/sh/tangled/repo/branches';
77-import type * as Compare from './lexicons/types/sh/tangled/repo/compare';
88-import type * as DescribeRepo from './lexicons/types/sh/tangled/repo/describeRepo';
99-import type * as Diff from './lexicons/types/sh/tangled/repo/diff';
1010-import type * as GetDefaultBranch from './lexicons/types/sh/tangled/repo/getDefaultBranch';
1111-import type * as Languages from './lexicons/types/sh/tangled/repo/languages';
1212-import type * as ListSecrets from './lexicons/types/sh/tangled/repo/listSecrets';
1313-import type * as Log from './lexicons/types/sh/tangled/repo/log';
1414-import type * as Tag from './lexicons/types/sh/tangled/repo/tag';
1515-import type * as Tags from './lexicons/types/sh/tangled/repo/tags';
1616-import type * as Tree from './lexicons/types/sh/tangled/repo/tree';
11+import type { BobbinContext, QueryValue, XrpcRequestInit } from "./client";
22+import { jsonGet, rawGet } from "./_request";
33+import type * as Archive from "./lexicons/types/sh/tangled/repo/archive";
44+import type * as Blob_ from "./lexicons/types/sh/tangled/repo/blob";
55+import type * as Branch from "./lexicons/types/sh/tangled/repo/branch";
66+import type * as Branches from "./lexicons/types/sh/tangled/repo/branches";
77+import type * as Compare from "./lexicons/types/sh/tangled/repo/compare";
88+import type * as DescribeRepo from "./lexicons/types/sh/tangled/repo/describeRepo";
99+import type * as Diff from "./lexicons/types/sh/tangled/repo/diff";
1010+import type * as GetDefaultBranch from "./lexicons/types/sh/tangled/repo/getDefaultBranch";
1111+import type * as Languages from "./lexicons/types/sh/tangled/repo/languages";
1212+import type * as ListSecrets from "./lexicons/types/sh/tangled/repo/listSecrets";
1313+import type * as Log from "./lexicons/types/sh/tangled/repo/log";
1414+import type * as Tag from "./lexicons/types/sh/tangled/repo/tag";
1515+import type * as Tags from "./lexicons/types/sh/tangled/repo/tags";
1616+import type * as Tree from "./lexicons/types/sh/tangled/repo/tree";
17171818// wrappers for bobbin's knot-proxied repo endpoints.
1919···2121 params as unknown as Record<string, QueryValue>;
22222323export const tree = (ctx: BobbinContext, params: Tree.$params, init?: XrpcRequestInit) =>
2424- jsonGet<Tree.$output>(ctx, 'sh.tangled.repo.tree', asParams(params), init);
2424+ jsonGet<Tree.$output>(ctx, "sh.tangled.repo.tree", asParams(params), init);
25252626export const blob = (ctx: BobbinContext, params: Blob_.$params, init?: XrpcRequestInit) =>
2727- jsonGet<Blob_.$output>(ctx, 'sh.tangled.repo.blob', asParams(params), init);
2727+ jsonGet<Blob_.$output>(ctx, "sh.tangled.repo.blob", asParams(params), init);
28282929export const branch = (ctx: BobbinContext, params: Branch.$params, init?: XrpcRequestInit) =>
3030- jsonGet<Branch.$output>(ctx, 'sh.tangled.repo.branch', asParams(params), init);
3030+ jsonGet<Branch.$output>(ctx, "sh.tangled.repo.branch", asParams(params), init);
31313232export const getDefaultBranch = (
3333 ctx: BobbinContext,
···3636) =>
3737 jsonGet<GetDefaultBranch.$output>(
3838 ctx,
3939- 'sh.tangled.repo.getDefaultBranch',
3939+ "sh.tangled.repo.getDefaultBranch",
4040 asParams(params),
4141 init
4242 );
···4545 ctx: BobbinContext,
4646 params: DescribeRepo.$params,
4747 init?: XrpcRequestInit
4848-) => jsonGet<DescribeRepo.$output>(ctx, 'sh.tangled.repo.describeRepo', asParams(params), init);
4848+) => jsonGet<DescribeRepo.$output>(ctx, "sh.tangled.repo.describeRepo", asParams(params), init);
49495050export const languages = (ctx: BobbinContext, params: Languages.$params, init?: XrpcRequestInit) =>
5151- jsonGet<Languages.$output>(ctx, 'sh.tangled.repo.languages', asParams(params), init);
5151+ jsonGet<Languages.$output>(ctx, "sh.tangled.repo.languages", asParams(params), init);
52525353export const listSecrets = (
5454 ctx: BobbinContext,
5555 params: ListSecrets.$params,
5656 init?: XrpcRequestInit
5757-) => jsonGet<ListSecrets.$output>(ctx, 'sh.tangled.repo.listSecrets', asParams(params), init);
5757+) => jsonGet<ListSecrets.$output>(ctx, "sh.tangled.repo.listSecrets", asParams(params), init);
58585959// schema-less json passthroughs; callers supply the shape.
60606161export const log = <T = unknown>(ctx: BobbinContext, params: Log.$params, init?: XrpcRequestInit) =>
6262- jsonGet<T>(ctx, 'sh.tangled.repo.log', asParams(params), init);
6262+ jsonGet<T>(ctx, "sh.tangled.repo.log", asParams(params), init);
63636464export const diff = <T = unknown>(
6565 ctx: BobbinContext,
6666 params: Diff.$params,
6767 init?: XrpcRequestInit
6868-) => jsonGet<T>(ctx, 'sh.tangled.repo.diff', asParams(params), init);
6868+) => jsonGet<T>(ctx, "sh.tangled.repo.diff", asParams(params), init);
69697070export const compare = <T = unknown>(
7171 ctx: BobbinContext,
7272 params: Compare.$params,
7373 init?: XrpcRequestInit
7474-) => jsonGet<T>(ctx, 'sh.tangled.repo.compare', asParams(params), init);
7474+) => jsonGet<T>(ctx, "sh.tangled.repo.compare", asParams(params), init);
75757676export const branches = <T = unknown>(
7777 ctx: BobbinContext,
7878 params: Branches.$params,
7979 init?: XrpcRequestInit
8080-) => jsonGet<T>(ctx, 'sh.tangled.repo.branches', asParams(params), init);
8080+) => jsonGet<T>(ctx, "sh.tangled.repo.branches", asParams(params), init);
81818282export const tags = <T = unknown>(
8383 ctx: BobbinContext,
8484 params: Tags.$params,
8585 init?: XrpcRequestInit
8686-) => jsonGet<T>(ctx, 'sh.tangled.repo.tags', asParams(params), init);
8686+) => jsonGet<T>(ctx, "sh.tangled.repo.tags", asParams(params), init);
87878888export const tag = <T = unknown>(ctx: BobbinContext, params: Tag.$params, init?: XrpcRequestInit) =>
8989- jsonGet<T>(ctx, 'sh.tangled.repo.tag', asParams(params), init);
8989+ jsonGet<T>(ctx, "sh.tangled.repo.tag", asParams(params), init);
90909191// raw response for streaming/download.
92929393export const archive = (ctx: BobbinContext, params: Archive.$params, init?: XrpcRequestInit) =>
9494- rawGet(ctx, 'sh.tangled.repo.archive', asParams(params), init);
9494+ rawGet(ctx, "sh.tangled.repo.archive", asParams(params), init);
+49-49
web/src/lib/api/load.test.ts
···11-import { describe, expect, it, vi } from 'vitest';
22-import { ClientResponseError } from './client';
33-import { createRequestCache, httpStatusFor, parallel, toHttpError } from './load';
11+import { describe, expect, it, vi } from "vitest";
22+import { ClientResponseError } from "./client";
33+import { createRequestCache, httpStatusFor, parallel, toHttpError } from "./load";
4455const cre = (status: number, error: string, message?: string): ClientResponseError =>
66 new ClientResponseError({ status, data: { error, message } });
7788-describe('httpStatusFor', () => {
99- it('passes through an in-band XRPC status', () => {
1010- expect(httpStatusFor(cre(404, 'RecordNotFound'))).toBe(404);
88+describe("httpStatusFor", () => {
99+ it("passes through an in-band XRPC status", () => {
1010+ expect(httpStatusFor(cre(404, "RecordNotFound"))).toBe(404);
1111 });
12121313- it('prefers an in-band status over the error-name mapping', () => {
1313+ it("prefers an in-band status over the error-name mapping", () => {
1414 // status 503 is in [400,599] so it wins even though the name maps to 404.
1515- expect(httpStatusFor(cre(503, 'RecordNotFound'))).toBe(503);
1515+ expect(httpStatusFor(cre(503, "RecordNotFound"))).toBe(503);
1616 });
17171818- it('maps the error name when the status is out of the 400-599 band', () => {
1919- expect(httpStatusFor(cre(200, 'RecordNotFound'))).toBe(404);
2020- expect(httpStatusFor(cre(200, 'InvalidRequest'))).toBe(400);
2121- expect(httpStatusFor(cre(200, 'UpstreamFailed'))).toBe(502);
2222- expect(httpStatusFor(cre(200, 'UpstreamGone'))).toBe(502);
2323- expect(httpStatusFor(cre(200, 'InvalidRecord'))).toBe(502);
2424- expect(httpStatusFor(cre(200, 'Overloaded'))).toBe(503);
2525- expect(httpStatusFor(cre(200, 'SomethingElse'))).toBe(500);
1818+ it("maps the error name when the status is out of the 400-599 band", () => {
1919+ expect(httpStatusFor(cre(200, "RecordNotFound"))).toBe(404);
2020+ expect(httpStatusFor(cre(200, "InvalidRequest"))).toBe(400);
2121+ expect(httpStatusFor(cre(200, "UpstreamFailed"))).toBe(502);
2222+ expect(httpStatusFor(cre(200, "UpstreamGone"))).toBe(502);
2323+ expect(httpStatusFor(cre(200, "InvalidRecord"))).toBe(502);
2424+ expect(httpStatusFor(cre(200, "Overloaded"))).toBe(503);
2525+ expect(httpStatusFor(cre(200, "SomethingElse"))).toBe(500);
2626 });
27272828- it('maps any non-XRPC cause to 500', () => {
2929- expect(httpStatusFor(new Error('boom'))).toBe(500);
3030- expect(httpStatusFor('a string')).toBe(500);
2828+ it("maps any non-XRPC cause to 500", () => {
2929+ expect(httpStatusFor(new Error("boom"))).toBe(500);
3030+ expect(httpStatusFor("a string")).toBe(500);
3131 expect(httpStatusFor(undefined)).toBe(500);
3232 expect(httpStatusFor({ status: 404 })).toBe(500);
3333 });
3434});
35353636-describe('toHttpError', () => {
3737- it('throws a SvelteKit error with the mapped status and the XRPC description', () => {
3636+describe("toHttpError", () => {
3737+ it("throws a SvelteKit error with the mapped status and the XRPC description", () => {
3838 let thrown: unknown;
3939 try {
4040- toHttpError(cre(404, 'RecordNotFound', 'no such repo'));
4040+ toHttpError(cre(404, "RecordNotFound", "no such repo"));
4141 } catch (e) {
4242 thrown = e;
4343 }
4444- expect(thrown).toMatchObject({ status: 404, body: { message: 'no such repo' } });
4444+ expect(thrown).toMatchObject({ status: 404, body: { message: "no such repo" } });
4545 });
46464747- it('falls back to the error name when the description is absent', () => {
4747+ it("falls back to the error name when the description is absent", () => {
4848 let thrown: unknown;
4949 try {
5050- toHttpError(cre(200, 'Overloaded'));
5050+ toHttpError(cre(200, "Overloaded"));
5151 } catch (e) {
5252 thrown = e;
5353 }
5454- expect(thrown).toMatchObject({ status: 503, body: { message: 'Overloaded' } });
5454+ expect(thrown).toMatchObject({ status: 503, body: { message: "Overloaded" } });
5555 });
56565757- it('uses the fallback message + 500 for a non-XRPC cause', () => {
5757+ it("uses the fallback message + 500 for a non-XRPC cause", () => {
5858 let thrown: unknown;
5959 try {
6060- toHttpError(new Error('boom'), 'could not load');
6060+ toHttpError(new Error("boom"), "could not load");
6161 } catch (e) {
6262 thrown = e;
6363 }
6464- expect(thrown).toMatchObject({ status: 500, body: { message: 'could not load' } });
6464+ expect(thrown).toMatchObject({ status: 500, body: { message: "could not load" } });
6565 });
6666});
67676868-describe('parallel', () => {
6969- it('preserves key/value pairing regardless of settle order', async () => {
6868+describe("parallel", () => {
6969+ it("preserves key/value pairing regardless of settle order", async () => {
7070 // deferred across a few microtasks (no real timer) so it settles after `fast`.
7171 const slow = Promise.resolve()
7272 .then(() => Promise.resolve())
7373- .then(() => 'slow');
7474- const result = await parallel({ fast: Promise.resolve('fast'), slow });
7575- expect(result).toEqual({ fast: 'fast', slow: 'slow' });
7373+ .then(() => "slow");
7474+ const result = await parallel({ fast: Promise.resolve("fast"), slow });
7575+ expect(result).toEqual({ fast: "fast", slow: "slow" });
7676 });
77777878- it('resolves to an empty object for no tasks', async () => {
7878+ it("resolves to an empty object for no tasks", async () => {
7979 await expect(parallel({})).resolves.toEqual({});
8080 });
81818282- it('rejects if any input promise rejects', async () => {
8282+ it("rejects if any input promise rejects", async () => {
8383 await expect(
8484- parallel({ a: Promise.resolve(1), b: Promise.reject(new Error('boom')) })
8585- ).rejects.toThrow('boom');
8484+ parallel({ a: Promise.resolve(1), b: Promise.reject(new Error("boom")) })
8585+ ).rejects.toThrow("boom");
8686 });
8787});
88888989-describe('createRequestCache', () => {
9090- it('invokes the loader once per key and shares the value', async () => {
8989+describe("createRequestCache", () => {
9090+ it("invokes the loader once per key and shares the value", async () => {
9191 const cache = createRequestCache();
9292 const load = vi.fn(async () => 42);
93939494- const first = await cache.run('k', load);
9595- const second = await cache.run('k', load);
9494+ const first = await cache.run("k", load);
9595+ const second = await cache.run("k", load);
96969797 expect(first).toBe(42);
9898 expect(second).toBe(42);
9999 expect(load).toHaveBeenCalledTimes(1);
100100 });
101101102102- it('shares one in-flight promise for concurrent same-key calls', () => {
102102+ it("shares one in-flight promise for concurrent same-key calls", () => {
103103 const cache = createRequestCache();
104104- const load = vi.fn(async () => 'v');
104104+ const load = vi.fn(async () => "v");
105105106106- const a = cache.run('k', load);
107107- const b = cache.run('k', load);
106106+ const a = cache.run("k", load);
107107+ const b = cache.run("k", load);
108108109109 expect(a).toBe(b);
110110 expect(load).toHaveBeenCalledTimes(1);
111111 });
112112113113- it('runs distinct keys independently', async () => {
113113+ it("runs distinct keys independently", async () => {
114114 const cache = createRequestCache();
115115 const load = vi.fn(async (key: string) => key.toUpperCase());
116116117117- await cache.run('a', () => load('a'));
118118- await cache.run('b', () => load('b'));
117117+ await cache.run("a", () => load("a"));
118118+ await cache.run("b", () => load("b"));
119119120120 expect(load).toHaveBeenCalledTimes(2);
121121 });
+9-9
web/src/lib/api/load.ts
···11-import { error, type NumericRange } from '@sveltejs/kit';
22-import { ClientResponseError } from './client';
11+import { error, type NumericRange } from "@sveltejs/kit";
22+import { ClientResponseError } from "./client";
3344export const httpStatusFor = (cause: unknown): number => {
55 if (cause instanceof ClientResponseError) {
66 if (cause.status >= 400 && cause.status <= 599) return cause.status;
77 switch (cause.error) {
88- case 'RecordNotFound':
88+ case "RecordNotFound":
99 return 404;
1010- case 'InvalidRequest':
1010+ case "InvalidRequest":
1111 return 400;
1212- case 'UpstreamFailed':
1313- case 'UpstreamGone':
1414- case 'InvalidRecord':
1212+ case "UpstreamFailed":
1313+ case "UpstreamGone":
1414+ case "InvalidRecord":
1515 return 502;
1616- case 'Overloaded':
1616+ case "Overloaded":
1717 return 503;
1818 default:
1919 return 500;
···2222 return 500;
2323};
24242525-export const toHttpError = (cause: unknown, fallbackMessage = 'Request failed'): never => {
2525+export const toHttpError = (cause: unknown, fallbackMessage = "Request failed"): never => {
2626 const status = httpStatusFor(cause) as NumericRange<400, 599>;
2727 const message =
2828 cause instanceof ClientResponseError ? (cause.description ?? cause.error) : fallbackMessage;
+29-29
web/src/lib/api/pagination.test.ts
···11-import { describe, expect, it, vi, type Mock } from 'vitest';
22-import { createBobbinClient, type BobbinContext } from './client';
33-import { collect, pages, paginateBy } from './pagination';
11+import { describe, expect, it, vi, type Mock } from "vitest";
22+import { createBobbinClient, type BobbinContext } from "./client";
33+import { collect, pages, paginateBy } from "./pagination";
4455-const NAME = 'sh.tangled.feed.listStars';
66-const PARAMS = { subject: 'did:plc:x' } as const;
55+const NAME = "sh.tangled.feed.listStars";
66+const PARAMS = { subject: "did:plc:x" } as const;
7788interface Page {
99 items: readonly { uri: string }[];
···1313const pageResponse = (page: Page): Response =>
1414 new Response(JSON.stringify(page), {
1515 status: 200,
1616- headers: { 'content-type': 'application/json' }
1616+ headers: { "content-type": "application/json" }
1717 });
18181919const makeCtx = (fetchMock: typeof globalThis.fetch): BobbinContext =>
2020- createBobbinClient({ serviceUrl: 'https://bobbin.test', fetch: fetchMock });
2020+ createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock });
21212222const cursorOf = (mock: Mock<typeof globalThis.fetch>, n: number): string | null =>
2323- new URL(String(mock.mock.calls[n][0])).searchParams.get('cursor');
2323+ new URL(String(mock.mock.calls[n][0])).searchParams.get("cursor");
24242525const threePageFetch = (): Mock<typeof globalThis.fetch> =>
2626 vi
2727 .fn<typeof globalThis.fetch>()
2828- .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'a' }, { uri: 'b' }], cursor: 'c1' }))
2929- .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'c' }], cursor: 'c2' }))
3030- .mockResolvedValueOnce(pageResponse({ items: [{ uri: 'd' }] }));
2828+ .mockResolvedValueOnce(pageResponse({ items: [{ uri: "a" }, { uri: "b" }], cursor: "c1" }))
2929+ .mockResolvedValueOnce(pageResponse({ items: [{ uri: "c" }], cursor: "c2" }))
3030+ .mockResolvedValueOnce(pageResponse({ items: [{ uri: "d" }] }));
31313232-describe('pages / items / collect follow the cursor', () => {
3333- it('pages() yields every page then stops when a page omits the cursor', async () => {
3232+describe("pages / items / collect follow the cursor", () => {
3333+ it("pages() yields every page then stops when a page omits the cursor", async () => {
3434 const fetchMock = threePageFetch();
3535 const seen: Page[] = [];
3636 for await (const p of pages(makeCtx(fetchMock), NAME, PARAMS)) seen.push(p as Page);
3737 expect(fetchMock).toHaveBeenCalledTimes(3);
3838- expect(seen.map((p) => p.cursor)).toEqual(['c1', 'c2', undefined]);
3838+ expect(seen.map((p) => p.cursor)).toEqual(["c1", "c2", undefined]);
3939 expect(cursorOf(fetchMock, 0)).toBeNull();
4040- expect(cursorOf(fetchMock, 1)).toBe('c1');
4141- expect(cursorOf(fetchMock, 2)).toBe('c2');
4040+ expect(cursorOf(fetchMock, 1)).toBe("c1");
4141+ expect(cursorOf(fetchMock, 2)).toBe("c2");
4242 });
4343});
44444545-describe('pagination caps', () => {
4646- it('maxPages caps the number of network round-trips even with unbounded cursors', async () => {
4545+describe("pagination caps", () => {
4646+ it("maxPages caps the number of network round-trips even with unbounded cursors", async () => {
4747 const fetchMock = vi
4848 .fn<typeof globalThis.fetch>()
4949- .mockImplementation(async () => pageResponse({ items: [{ uri: 'x' }], cursor: 'always' }));
4949+ .mockImplementation(async () => pageResponse({ items: [{ uri: "x" }], cursor: "always" }));
5050 const out = await collect(makeCtx(fetchMock), NAME, PARAMS, { maxPages: 2 });
5151 expect(fetchMock).toHaveBeenCalledTimes(2);
5252 expect(out).toHaveLength(2);
5353 });
54545555- it('collect({ max }) caps items mid-page without fetching the next page', async () => {
5555+ it("collect({ max }) caps items mid-page without fetching the next page", async () => {
5656 const fetchMock = vi
5757 .fn<typeof globalThis.fetch>()
5858 .mockResolvedValueOnce(
5959- pageResponse({ items: [{ uri: 'a' }, { uri: 'b' }, { uri: 'c' }], cursor: 'c1' })
5959+ pageResponse({ items: [{ uri: "a" }, { uri: "b" }, { uri: "c" }], cursor: "c1" })
6060 );
6161 const out = await collect(makeCtx(fetchMock), NAME, PARAMS, { max: 2 });
6262- expect(out).toEqual([{ uri: 'a' }, { uri: 'b' }]);
6262+ expect(out).toEqual([{ uri: "a" }, { uri: "b" }]);
6363 expect(fetchMock).toHaveBeenCalledTimes(1);
6464 });
6565});
66666767-describe('paginateBy', () => {
6868- it('treats cursor: null as the end of the stream', async () => {
6767+describe("paginateBy", () => {
6868+ it("treats cursor: null as the end of the stream", async () => {
6969 const load = vi.fn(async () => ({
7070 items: [1, 2] as const,
7171 cursor: null
···7676 expect(load).toHaveBeenCalledTimes(1);
7777 });
78787979- it('follows a string cursor and forwards it to the loader', async () => {
7979+ it("follows a string cursor and forwards it to the loader", async () => {
8080 const load = vi
8181 .fn<
8282 (
8383 cursor: string | undefined
8484 ) => Promise<{ items: readonly number[]; cursor?: string | null }>
8585 >()
8686- .mockResolvedValueOnce({ items: [1], cursor: 'p2' })
8686+ .mockResolvedValueOnce({ items: [1], cursor: "p2" })
8787 .mockResolvedValueOnce({ items: [2], cursor: undefined });
8888 const out: number[] = [];
8989 for await (const n of paginateBy(load)) out.push(n);
9090 expect(out).toEqual([1, 2]);
9191 expect(load).toHaveBeenCalledTimes(2);
9292 expect(load.mock.calls[0][0]).toBeUndefined();
9393- expect(load.mock.calls[1][0]).toBe('p2');
9393+ expect(load.mock.calls[1][0]).toBe("p2");
9494 });
95959696- it('maxPages caps loader invocations', async () => {
9696+ it("maxPages caps loader invocations", async () => {
9797 const load = vi.fn(async () => ({
9898 items: [0],
9999- cursor: 'always'
9999+ cursor: "always"
100100 }));
101101 const out: number[] = [];
102102 for await (const n of paginateBy(load, { maxPages: 3 })) out.push(n);
+8-8
web/src/lib/api/pagination.ts
···11-import type { XRPCQueries } from '@atcute/lexicons/ambient';
11+import type { XRPCQueries } from "@atcute/lexicons/ambient";
22import type {
33 InferInput,
44 InferOutput,
55 ObjectSchema,
66 XRPCLexBodyParam
77-} from '@atcute/lexicons/validations';
88-import { ok, type BobbinContext, type XrpcRequestInit } from './client';
77+} from "@atcute/lexicons/validations";
88+import { ok, type BobbinContext, type XrpcRequestInit } from "./client";
991010export type QueryOutput<TName extends keyof XRPCQueries> =
1111- XRPCQueries[TName]['output'] extends XRPCLexBodyParam
1212- ? InferOutput<XRPCQueries[TName]['output']['schema']>
1111+ XRPCQueries[TName]["output"] extends XRPCLexBodyParam
1212+ ? InferOutput<XRPCQueries[TName]["output"]["schema"]>
1313 : never;
14141515export type QueryParams<TName extends keyof XRPCQueries> =
1616- XRPCQueries[TName]['params'] extends ObjectSchema
1717- ? InferInput<XRPCQueries[TName]['params']>
1616+ XRPCQueries[TName]["params"] extends ObjectSchema
1717+ ? InferInput<XRPCQueries[TName]["params"]>
1818 : Record<string, never>;
19192020type CursorPage = { items: readonly unknown[]; cursor?: string };
···2727export type PageItem<TName extends PaginatedQuery> =
2828 QueryOutput<TName> extends { items: readonly (infer I)[] } ? I : never;
29293030-export type PageParams<TName extends PaginatedQuery> = Omit<QueryParams<TName>, 'cursor'>;
3030+export type PageParams<TName extends PaginatedQuery> = Omit<QueryParams<TName>, "cursor">;
31313232export interface PaginateOptions extends XrpcRequestInit {
3333 /** stop after this many network round-trips. */
+8-8
web/src/lib/api/profile.ts
···11-import { ok } from '@atcute/client';
22-import { mainSchema as putRecordSchema } from '@atcute/atproto/types/repo/putRecord';
33-import type { Nsid, RecordKey } from '@atcute/lexicons/syntax';
44-import type { OAuthUserAgent } from '@atcute/oauth-browser-client';
55-import { createClient } from '$lib/auth/agent';
66-import type { ProfileRecord } from './records';
11+import { ok } from "@atcute/client";
22+import { mainSchema as putRecordSchema } from "@atcute/atproto/types/repo/putRecord";
33+import type { Nsid, RecordKey } from "@atcute/lexicons/syntax";
44+import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
55+import { createClient } from "$lib/auth/agent";
66+import type { ProfileRecord } from "./records";
7788-const PROFILE_COLLECTION = 'sh.tangled.actor.profile' as Nsid;
88+const PROFILE_COLLECTION = "sh.tangled.actor.profile" as Nsid;
991010// the profile record lives at the fixed rkey `self`; put upserts it.
1111export const putProfile = async (agent: OAuthUserAgent, record: ProfileRecord): Promise<void> => {
···1515 input: {
1616 repo: agent.sub,
1717 collection: PROFILE_COLLECTION,
1818- rkey: 'self' as RecordKey,
1818+ rkey: "self" as RecordKey,
1919 record
2020 }
2121 })