fix: report OPFS completion honestly and bound public cache staging

A durable write whose journal transaction could not be completed returned
plain success with `SUCCEEDED` telemetry. The payload was committed but
the transaction stayed `COMMITTED`, so the reconcile backlog and its quota
pressure grew while every caller was told the write had settled. That is
now a `RECONCILE` failure with the effect certainty preserved, and an
unfinished delete is observed `DEGRADED` rather than clean.

The worker seam lost causes in both directions. A bootstrap failure
answered every request with kind `CAPABILITIES`, so the gateway read a
kind mismatch and replaced the real `BLOCKED` or `QUOTA_EXCEEDED` with a
generic `UNSUPPORTED`; the envelope's correlation is now captured once at
the listener. On the client, the pending row and its timer were released
before the reply was decoded, so a trap that threw inside the decoder left
the public promise pending with nothing left to time it out, and a
throwing `requestId` getter produced a timeout instead of a prompt
protocol failure.

Public cache staging handed its signal to each `Request` and called that
ownership. A fetch that ignored it held the mutation lock forever, and a
digest that finished after the abort still wrote both the asset and the
activation marker — publishing a release nobody was waiting for. One
terminal owner now covers the whole staging body and every await
re-checks it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-15 01:25:26 +09:00
co-authored by Claude Opus 5
parent df18349682
commit 632b230c82
8 changed files with 692 additions and 42 deletions
+144
View File
@@ -528,6 +528,150 @@ describe("public response Cache Storage adapter", () => {
await expect(cacheStorage.keys()).resolves.toEqual([]);
});
/**
* NS-08. Handing the signal to each `Request` only asked a cooperative fetch
* to stop. A stream that ignored it held the mutation lock forever, and work
* that finished after the abort still wrote its asset and its marker.
*/
it("does not wait for a non-cooperative fetch after the caller aborts", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([1, 2, 3, 4]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/never-settles.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes) },
};
const manifest = await manifestFor("never-settles", [asset], policy);
const fetchStarted = deferred<void>();
let locksHeld = 0;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: cacheStorage as unknown as CacheStorage,
crypto: globalThis.crypto,
mutationLock: {
async run(signal, operation) {
locksHeld += 1;
try {
return await operation();
} finally {
locksHeld -= 1;
}
},
},
policy,
fetcher: () => {
fetchStarted.resolve(undefined);
// Ignores the signal entirely.
return new Promise<Response>(() => {});
},
});
const controller = new AbortController();
const staging = adapter.admin.stageRelease(manifest, {
signal: controller.signal,
});
await fetchStarted.promise;
controller.abort();
await expect(staging).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
expect(locksHeld).toBe(0);
await expect(cacheStorage.keys()).resolves.toEqual([]);
});
it("writes neither asset nor marker when a digest completes after the abort", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(
"https://assets.example.test",
);
const bytes = new Uint8Array([9, 9, 9, 9]);
const asset: PublicCacheAsset = {
absoluteUrl: "https://assets.example.test/late-digest.js",
expectedByteLength: bytes.byteLength,
expectedContentType: "application/javascript",
integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes) },
};
const manifest = await manifestFor("late-digest", [asset], policy);
const digestStarted = deferred<void>();
const releaseDigest = deferred<void>();
let delayFirstDigest = true;
const realCrypto = globalThis.crypto;
const delayedCrypto = {
subtle: {
async digest(
algorithm: AlgorithmIdentifier,
data: BufferSource,
): Promise<ArrayBuffer> {
if (delayFirstDigest) {
delayFirstDigest = false;
digestStarted.resolve(undefined);
await releaseDigest.promise;
}
return await realCrypto.subtle.digest(algorithm, data);
},
},
} as unknown as Crypto;
let puts = 0;
const adapter = createPublicResponseCacheAdapter({
cacheStorage: new Proxy(cacheStorage, {
get(target, key, receiver) {
if (key === "open") {
return async (name: string) => {
const cache = await target.open(name);
return new Proxy(cache, {
get(cacheTarget, cacheKey, cacheReceiver) {
if (cacheKey === "put") {
return async (...args: readonly unknown[]) => {
puts += 1;
return await (
cacheTarget.put as (
...values: readonly unknown[]
) => Promise<void>
)(...args);
};
}
return Reflect.get(cacheTarget, cacheKey, cacheReceiver);
},
});
};
}
return Reflect.get(target, key, receiver);
},
}) as unknown as CacheStorage,
crypto: delayedCrypto,
mutationLock: immediateLock,
policy,
fetcher: async () =>
new Response(bytes, {
headers: {
"cache-control": "public, max-age=60",
"content-type": "application/javascript",
},
}),
});
const controller = new AbortController();
const staging = adapter.admin.stageRelease(manifest, {
signal: controller.signal,
});
await digestStarted.promise;
controller.abort();
releaseDigest.resolve(undefined);
await expect(staging).resolves.toMatchObject({
ok: false,
error: { code: "ABORTED" },
});
// Neither the asset nor the activation marker may be written by work the
// abort already disowned.
expect(puts).toBe(0);
});
it("keeps the original activation signal while waiting for the mutation lock", async () => {
const cacheStorage = new MemoryCacheStorage();
const policy = createDefaultPublicCachePolicy(