clone

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

dependabot/npm_and_yarn/npm-f01a459b0f
bindws / src / repository-access.ts
repository-access.ts 4.0K
1// One live relay operation owns repository authority across awaits. Async2// context carries the owner token; the active lease grants admission. This3// is neither a durable lock nor authority to collect retained Git objects.4import { AsyncLocalStorage } from "node:async_hooks";56type Kind = "git" | "control" | "alarm" | "event" | "teardown";7type Lease = { kind: Kind; holders: number };89export class RepositoryAccess {10 private readonly context = new AsyncLocalStorage<Lease>();11 private active?: Lease;1213 get busy() { return this.active !== undefined; }14 get owned() { return this.active !== undefined && this.context.getStore() === this.active; }15 get blocked() { return this.busy && !this.owned; }16 get kind() { return this.active?.kind; }1718 async run<T>(kind: Kind, work: () => Promise<T> | T, refused: () => Promise<T> | T): Promise<T> {19 if (this.blocked) return refused();20 const lease = this.active ??= { kind, holders: 0 };21 lease.holders++;22 try { return await this.context.run(lease, work); }23 finally { this.release(lease); }24 }2526 // Fuel receipts are provider-signed credits, not ordinary relay writes.27 // They may be recorded while a Git transfer holds the long-lived lease so28 // a payment can unblock that transfer. Other exclusive operations still29 // fence them, and an idle relay uses the normal event lease.30 async runFuel<T>(work: () => Promise<T> | T, refused: () => Promise<T> | T): Promise<T> {31 if (this.active && this.active.kind !== "git") {32 // Preserve the existing nested-work behavior for a caller that already33 // owns control (for example, the authenticated bridge route). A34 // separate caller remains fenced by the active operation.35 if (this.owned) return this.run("event", work, refused);36 return refused();37 }38 if (this.active?.kind === "git") {39 const lease = this.active;40 lease.holders++;41 try { return await this.context.run(lease, work); }42 finally { this.release(lease); }43 }44 return this.run("event", work, refused);45 }4647 // A streamed Git response still reads repository state after fetch returns.48 // Carry authority into each pull, and release it on EOF, error or disconnect.49 async response(kind: Kind, work: () => Promise<Response>, refused: () => Response): Promise<Response> {50 if (this.blocked) return refused();51 const lease = this.active ??= { kind, holders: 0 };52 lease.holders++;53 let released = false;54 const finish = () => {55 if (!released) { released = true; this.release(lease); }56 };57 try {58 const response = await this.context.run(lease, work);59 if (!response.body) { finish(); return response; }60 const reader = response.body.getReader();61 const body = new ReadableStream<Uint8Array>({62 pull: controller => this.context.run(lease, async () => {63 try {64 const result = await reader.read();65 if (result.done) { reader.releaseLock(); controller.close(); finish(); }66 else controller.enqueue(result.value);67 } catch (error) {68 try { await reader.cancel(error); } catch { /* Preserve the read error. */ }69 finally { reader.releaseLock(); controller.error(error); finish(); }70 }71 }),72 cancel: reason => this.context.run(lease, async () => {73 try { await reader.cancel(reason); } finally { reader.releaseLock(); finish(); }74 }),75 }, { highWaterMark: 0 });76 return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });77 } catch (error) { finish(); throw error; }78 }7980 sync<T>(kind: Kind, work: () => T, refused: () => T): T {81 if (this.blocked) return refused();82 const lease = this.active ??= { kind, holders: 0 };83 lease.holders++;84 try { return this.context.run(lease, work); }85 finally { this.release(lease); }86 }8788 private release(lease: Lease) {89 if (--lease.holders === 0 && this.active === lease) this.active = undefined;90 }91}