fix(director): discard an asset cancelled during reader cleanup

The finally block awaits reader.cancel(), so a cancel arriving there still let
the bytes return. Rechecks the signal after cleanup, with a test that keeps the
caller's abort reason, and wraps the current-state note.
This commit is contained in:
Sameh Khamis committed 2026-10-14 19:23:12 -07:00
1 parent d42f5d4fd2
commit bf4edb5bdb
3 files changed
+42 -1

No files matched your search

@@ -40,3 +40,40 @@ test('cancellation during the terminating read cannot return a scene asset', asy
assert.equal(released, 1);
}
});
test('cancellation during reader cleanup cannot return a scene asset', async () => {
const controller = new AbortController();
const reason = new DOMException('scene changed', 'AbortError');
let released = 0;
const source = createAssetDirectorySource({
baseUrl: 'https://fixture.example/assets/',
fetchImpl: async () => ({
ok: true,
headers: new Headers(),
body: {
getReader() {
let reads = 0;
return {
async read() {
reads++;
return reads === 1
? { done: false, value: new Uint8Array([1]) }
: { done: true };
},
async cancel() {
controller.abort(reason);
},
releaseLock() {
released++;
},
};
},
},
}),
});
await assert.rejects(
source({ path: 'scene.bin', signal: controller.signal }),
(error) => error === reason,
);
assert.equal(released, 1);
});
+2
View File
@@ -51,6 +51,8 @@ export function createAssetDirectorySource({
await reader.cancel().catch(() => {});
reader.releaseLock();
}
// Cleanup awaits too; a cancel during it must still discard the asset.
signal?.throwIfAborted();
const bytes = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {