clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

grasp: live repository ownership follows nested work across awaits

Dami committed 6b929a9
commit
6b929a9d808c356035c07ebfee06102d8c396c47
parent
c62c853a7bbe99c8e3ac3b99ef48c3bad03247fb
author
Dami <dami.etoile@gmail.com>
2 files changed, +109 -0
Asrc/repository-access.ts37+++++++++++++++
Atest/unit/repository-access.test.ts72++++++++++++++++++++++++++++++
A src/repository-access.ts +37 -0
A test/unit/repository-access.test.ts +72 -0
@@ -0,0 +1,72 @@
1+import { describe, expect, it } from "vitest";2+import { RepositoryAccess } from "../../src/repository-access.ts";3+4+const latch = () => {5+ let release!: () => void;6+ const promise = new Promise<void>((resolve) => { release = resolve; });7+ return { promise, release };8+};9+const refused = () => "refused";10+11+describe("Repository admission", () => {12+ it("keeps unrelated operations out across awaits and permits owned nested work", async () => {13+ const access = new RepositoryAccess();14+ const entered = latch();15+ const finish = latch();16+ const active = access.run("git", async () => {17+ expect(access.owned).toBe(true);18+ expect(access.sync("event", () => "nested", refused)).toBe("nested");19+ entered.release();20+ await finish.promise;21+ expect(await access.run("control", () => "nested async", refused)).toBe("nested async");22+ expect(access.kind).toBe("git");23+ return "done";24+ }, refused);25+ await entered.promise;26+ expect(access.blocked).toBe(true);27+ expect(access.sync("event", () => "unexpected", refused)).toBe("refused");28+ expect(await access.run("alarm", () => "unexpected", refused)).toBe("refused");29+ finish.release();30+ expect(await active).toBe("done");31+ expect(access.busy).toBe(false);32+ });33+34+ it("releases synchronous and asynchronous owners after failures", async () => {35+ const access = new RepositoryAccess();36+ expect(() => access.sync("event", () => { throw new Error("sync"); }, refused)).toThrow("sync");37+ expect(access.busy).toBe(false);38+ await expect(access.run("git", async () => { throw new Error("async"); }, refused)).rejects.toThrow("async");39+ expect(access.busy).toBe(false);40+ });41+42+ it("retains an entered child until it settles even when the parent returns", async () => {43+ const access = new RepositoryAccess();44+ const finish = latch();45+ let child!: Promise<string>;46+ await access.run("control", () => {47+ child = access.run("event", async () => { await finish.promise; return "child"; }, refused);48+ return "parent";49+ }, refused);50+ expect(access.blocked).toBe(true);51+ finish.release();52+ expect(await child).toBe("child");53+ expect(access.busy).toBe(false);54+ });55+56+ it("never lets a stale async context borrow a different active owner", async () => {57+ const access = new RepositoryAccess();58+ const resume = latch();59+ let stale!: Promise<string>;60+ await access.run("control", () => {61+ stale = resume.promise.then(() => access.run("event", () => "unexpected", refused));62+ return "original";63+ }, refused);64+ const finish = latch();65+ const active = access.run("git", async () => { await finish.promise; return "current"; }, refused);66+ resume.release();67+ expect(await stale).toBe("refused");68+ finish.release();69+ await active;70+ expect(access.busy).toBe(false);71+ });72+});