fix: make OPFS finalization and public cache repair failure-atomic

STO-RR-01. finalizePut re-acquired the origin mutation lease it was already
holding. A Web Lock is not reentrant, so an ordinary PUT stopped for good at
FINALIZE; it now calls the locked cleanup directly. A strict non-reentrant fake
lease manager pins one acquire and one release per finalization. The adapter no
longer reports a failed finalization as a plain write success either: the
journal row stays COMMITTED for reconciliation, but the caller is told the
write did not settle.

STO-RR-02. A failure raised while serving a validated request now carries that
request's kind. Defaulting every catch to CAPABILITIES made the client's own
expected-kind check reject genuine quota, integrity and abort failures as
protocol breaches and report them as UNSUPPORTED. Only an envelope the runtime
could not read still answers at protocol level.

STO-RR-03. The worker client decodes a response instead of adopting it: exact
own-data descriptors, the negotiated protocol version, the exact awaited kind,
a code inside the closed BrowserDataFailure set and a boolean retryable. An
accessor, a proxy trap, an inherited or extra field and an unknown code all
close the call as UNSUPPORTED rather than leaving it to time out.

STO-RR-04. A marker read that fails transiently is unknown, not damaged, so it
no longer deletes the candidate that may be serving traffic. Only a confirmed
corrupt or missing marker enters the repair path.

STO-RR-05. Staging never deletes a candidate it did not create. A repair
replaces exact entries in place, so a failed fetch leaves every healthy asset
and the active release usable; a candidate this call created is still removed
on failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 14:00:41 +09:00
co-authored by Claude Opus 5
parent ca210d3bc5
commit 6a8281a941
8 changed files with 785 additions and 76 deletions
+9 -2
View File
@@ -674,7 +674,13 @@ describe("OPFS byte-store coordinator", () => {
expect(stored?.physicalSchemaVersion).toBe(1);
});
it("keeps a committed journal row for reconciliation when cleanup fails", async () => {
/**
* STO-RR-01. Finalization runs after the commit fence, so the payload is
* durable and the journal row must survive for reconciliation. What the
* caller must not be told is that the write settled: the previous generation
* and the staging directory are still there.
*/
it("reports a failed finalization instead of a plain success", async () => {
const journal = createJournal();
const worker = createWorker({
async finalizePut() {
@@ -706,7 +712,8 @@ describe("OPFS byte-store coordinator", () => {
source: sourceFrom(new Uint8Array([1])),
});
expect(result.ok).toBe(true);
expect(result.ok).toBe(false);
expect(result.ok ? null : result.error.operation).toBe("OBJECT_WRITE");
expect(
journal.transactions.get("transaction_12345678")?.phase,
).toBe("COMMITTED");