Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 6f38f174bcda2a420c2c07374d5d4c5e98d7b78c
parent 721530d506ba681198278d641dc774b0ded3ce81
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 03:27:31 -0400

site: patchSite, a locked read-modify-write of some keys of site.json

The site is read inside the file lock and the patch applied to it, so a
writer that owns a few keys never writes back a stale copy of the rest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/lib/site.ts | 29++++++++++++++++++++++++++++-
Mcommon/lib/siteSchema.test.ts | 33++++++++++++++++++++++++++++++++-
2 files changed, 60 insertions(+), 2 deletions(-)

diff --git a/common/lib/site.ts b/common/lib/site.ts @@ -11,7 +11,7 @@ import { type SocialLink, } from "./settings"; import { socialLinksForSave } from "./socialLinks"; -import { readJsonFileSync, writeJsonAtomic } from "./jsonFile-server"; +import { readJsonFileSync, withJsonFileLock, writeJsonAtomic } from "./jsonFile-server"; import { isListedSite, isPrivateSite, @@ -267,6 +267,33 @@ export async function writeSite( ); } +// PATCH SOME KEYS OF A SITE — the writer for a surface that owns a few keys and +// none of the others (the Reports tab's `reports`). The site is read from disk +// INSIDE the file lock and the patch is applied to that, so a patch never +// writes back a copy of the other keys read earlier, and two patches never +// interleave. A function patch is given the site as it is now (to reorder a +// list it did not read itself). Validated and written by writeSite. Null when +// the site has no site.json: a patch never creates a site. +export async function patchSite( + siteId: string, + patch: + | Partial<Omit<Site, "siteId">> + | ((current: Site) => Partial<Omit<Site, "siteId">>), + paths: Paths = getPaths(), +): Promise<Site | null> { + if (!isValidSiteId(siteId)) { + throw new Error(`Invalid site id: ${String(siteId)}`); + } + const file = siteConfigFile(paths, siteId); + return withJsonFileLock(file, async () => { + if (!fs.existsSync(file)) return null; + const current = getSite(siteId, paths); + const changes = typeof patch === "function" ? patch(current) : patch; + await writeSite({ ...current, ...changes, siteId }, paths); + return getSite(siteId, paths); + }); +} + export async function deleteSite( siteId: string, paths: Paths = getPaths(), diff --git a/common/lib/siteSchema.test.ts b/common/lib/siteSchema.test.ts @@ -18,7 +18,7 @@ import { siteToDisk, type Site, } from "./siteSchema"; -import { getSite, resolveRelatedSites, siteConfigFile, writeSite } from "./site"; +import { getSite, patchSite, resolveRelatedSites, siteConfigFile, writeSite } from "./site"; import type { Paths } from "./paths"; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -409,3 +409,34 @@ test("writeSite → getSite round-trips, and the file holds only non-defaults", assert.equal("publish" in disk, false); assert.equal("reports" in disk, false); }); + +test("patchSite applies a patch to the site on disk now, keeps every other key, and never creates a site", async () => { + const paths = scratchPaths(await mkdtemp(path.join(os.tmpdir(), "site-"))); + assert.equal(await patchSite("s", { reports: ["one"] }, paths), null); + assert.equal(fs.existsSync(siteConfigFile(paths, "s")), false); + + const site = parseSite("s", { + siteTitle: "Mine", + archives: false, + publish: "cited", + reports: ["one", "two"], + }); + await writeSite(site, paths); + // Another writer changes a key after this caller last read the site: the + // patch is applied to the file, not to the stale copy. + await writeSite({ ...site, siteTitle: "Renamed" }, paths); + const patched = await patchSite("s", { reports: ["two", "one"] }, paths); + assert.deepEqual(patched, { ...site, siteTitle: "Renamed", reports: ["two", "one"] }); + assert.deepEqual(getSite("s", paths), patched); + + // A function patch sees the site as it is; two patches do not interleave. + await Promise.all([ + patchSite("s", (cur) => ({ reports: [...(cur.reports ?? []), "three"] }), paths), + patchSite("s", (cur) => ({ reports: [...(cur.reports ?? []), "four"] }), paths), + ]); + assert.deepEqual(getSite("s", paths).reports, ["two", "one", "three", "four"]); + // The id is the directory's, whatever a patch says. + await patchSite("s", { siteId: "other" } as Partial<Site>, paths); + assert.equal(getSite("s", paths).siteId, "s"); + assert.equal(fs.existsSync(siteConfigFile(paths, "other")), false); +});