From ec594fae2fdcdf60869fdf052a45a11e931ac26c Mon Sep 17 00:00:00 2001 From: dawn Date: Tue, 15 Sep 2026 05:56:36 +0300 Subject: [PATCH] web: resume a repo delete whose record is already gone Signed-off-by: dawn --- web/src/lib/api/repoSettings.test.ts | 26 ++++++++++++++++++++++++++ web/src/lib/api/repoSettings.ts | 12 ++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/web/src/lib/api/repoSettings.test.ts b/web/src/lib/api/repoSettings.test.ts index cbad26c9b..4cdd2b293 100644 --- a/web/src/lib/api/repoSettings.test.ts +++ b/web/src/lib/api/repoSettings.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ClientResponseError } from "@atcute/client"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { BobbinContext } from "$lib/api/client"; import type { RecordView, RepoRecord } from "$lib/api/records"; @@ -146,6 +147,31 @@ describe("deleteRepo", () => { }); }); + it("carries on when the record is already gone", async () => { + deleteRecord.mockRejectedValueOnce( + new ClientResponseError({ + status: 400, + data: { error: "RecordNotFound", message: "could not locate record" } + }) + ); + + await api.deleteRepo(agent, bobbin, location); + + expect(post).toHaveBeenCalled(); + }); + + it("stops when the record moved under a concurrent edit", async () => { + deleteRecord.mockRejectedValueOnce( + new ClientResponseError({ + status: 400, + data: { error: "InvalidSwap", message: "record moved" } + }) + ); + + await expect(api.deleteRepo(agent, bobbin, location)).rejects.toThrow(/InvalidSwap/); + expect(post).not.toHaveBeenCalled(); + }); + it("reports a knot that refuses after the record is gone", async () => { post.mockRejectedValueOnce(new Error("repo already exists: core")); diff --git a/web/src/lib/api/repoSettings.ts b/web/src/lib/api/repoSettings.ts index 1bb6dbb50..f278db258 100644 --- a/web/src/lib/api/repoSettings.ts +++ b/web/src/lib/api/repoSettings.ts @@ -1,4 +1,4 @@ -import { ok } from "@atcute/client"; +import { ClientResponseError, ok } from "@atcute/client"; import type { Cid, Nsid, ResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { awaitDeletedRecord, awaitIndexedRecord } from "$lib/api/awaitRecord"; @@ -93,6 +93,10 @@ export const removeCollaborator = async ( ); }; +const isRecordGone = (cause: unknown): boolean => + cause instanceof ClientResponseError && + (cause.status === 404 || cause.error === "RecordNotFound"); + export interface RepoLocation { uri: string; knot: string; @@ -108,7 +112,11 @@ export const deleteRepo = async ( repo: RepoLocation ): Promise => { const rkey = rkeyFromUri(repo.uri); - await deleteRecord(agent, REPO_COLLECTION, rkey, repo.cid); + try { + await deleteRecord(agent, REPO_COLLECTION, rkey, repo.cid); + } catch (cause) { + if (!isRecordGone(cause)) throw cause; + } try { await ok( serviceClient(agent, repo.knot).xrpc.post(deleteRepoSchema.nsid, { -- 2.51.2