feat(presentation): add cancellable frame renderer

This commit is contained in:
2026-09-05 12:24:48 +08:00
parent 1e168ae81e
commit 8b7efa7de0
2 changed files with 330 additions and 0 deletions
@@ -0,0 +1,263 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { FrameRenderer } from '../../YouleNexus/assets/framework/presentation/frame-renderer.ts';
test('coalesces frames and invalidates callbacks even if scheduler delivers late', () => {
const callbacks: (() => void)[] = [];
let cancelled = 0;
let rendered = 0;
const renderer = new FrameRenderer({
request(cb) { callbacks.push(cb); return () => { cancelled++; }; },
}, () => { rendered++; }, e => { throw e; });
renderer.request(); renderer.request();
assert.equal(callbacks.length, 1);
renderer.flush();
assert.equal(rendered, 1);
callbacks[0]!();
assert.equal(rendered, 1);
renderer.request();
renderer.dispose(); renderer.dispose();
callbacks[1]!();
assert.equal(rendered, 1);
assert.equal(cancelled, 2);
});
test('releases the current frame before render can request the next frame', () => {
const callbacks: (() => void)[] = [];
let rendered = 0;
let renderer!: FrameRenderer;
renderer = new FrameRenderer({
request(callback) { callbacks.push(callback); return () => {}; },
}, () => {
rendered += 1;
renderer.request();
}, error => { throw error; });
renderer.request();
callbacks[0]!();
assert.equal(rendered, 1);
assert.equal(callbacks.length, 2);
callbacks[1]!();
assert.equal(rendered, 2);
assert.equal(callbacks.length, 3);
});
test('dispose during render prevents a reentrant frame request', () => {
const callbacks: (() => void)[] = [];
let rendered = 0;
let renderer!: FrameRenderer;
renderer = new FrameRenderer({
request(callback) { callbacks.push(callback); return () => {}; },
}, () => {
rendered += 1;
renderer.dispose();
renderer.request();
}, error => { throw error; });
renderer.request();
callbacks[0]!();
assert.equal(rendered, 1);
assert.equal(callbacks.length, 1);
renderer.flush();
assert.equal(rendered, 1);
});
test('a throwing dispose cancellation still makes every callback stale', () => {
const callbacks: (() => void)[] = [];
const cancelError = new Error('cancel failed');
let rendered = 0;
const renderer = new FrameRenderer({
request(callback) {
callbacks.push(callback);
return () => { throw cancelError; };
},
}, () => { rendered += 1; }, error => { throw error; });
renderer.request();
let thrown: unknown;
try {
renderer.dispose();
} catch (error) {
thrown = error;
}
assert.equal(thrown, cancelError);
callbacks[0]!();
assert.equal(rendered, 0);
renderer.request();
assert.equal(callbacks.length, 1);
});
test('a throwing flush cancellation invalidates the old frame before rethrowing', () => {
const callbacks: (() => void)[] = [];
const cancelError = new Error('cancel failed');
let rendered = 0;
const renderer = new FrameRenderer({
request(callback) {
callbacks.push(callback);
return () => { throw cancelError; };
},
}, () => { rendered += 1; }, error => { throw error; });
renderer.request();
let thrown: unknown;
try {
renderer.flush();
} catch (error) {
thrown = error;
}
assert.equal(thrown, cancelError);
callbacks[0]!();
assert.equal(rendered, 0);
renderer.request();
assert.equal(callbacks.length, 2);
});
test('flush does not render when cancellation disposes the renderer', () => {
let rendered = 0;
let renderer!: FrameRenderer;
renderer = new FrameRenderer({
request() { return () => { renderer.dispose(); }; },
}, () => { rendered += 1; }, error => { throw error; });
renderer.request();
renderer.flush();
assert.equal(rendered, 0);
});
test('a cancellation that reenters flush supersedes the outer render', () => {
let rendered = 0;
let renderer!: FrameRenderer;
renderer = new FrameRenderer({
request() { return () => { renderer.flush(); }; },
}, () => { rendered += 1; }, error => { throw error; });
renderer.request();
renderer.flush();
assert.equal(rendered, 1);
});
test('reports an asynchronous render error once and terminates future frames', () => {
const callbacks: (() => void)[] = [];
const renderError = new Error('render failed');
const faults: unknown[] = [];
let rendered = 0;
const renderer = new FrameRenderer({
request(callback) { callbacks.push(callback); return () => {}; },
}, () => {
rendered += 1;
throw renderError;
}, error => { faults.push(error); });
renderer.request();
let thrown: unknown;
try {
callbacks[0]!();
} catch (error) {
thrown = error;
}
assert.equal(thrown, undefined);
assert.deepEqual(faults, [renderError]);
callbacks[0]!();
renderer.request();
assert.equal(rendered, 1);
assert.deepEqual(faults, [renderError]);
assert.equal(callbacks.length, 1);
});
test('aggregates an asynchronous render error with reentrant cancellation failure', () => {
const callbacks: (() => void)[] = [];
const renderError = new Error('render failed');
const cancelError = new Error('cancel failed');
const faults: unknown[] = [];
let rendered = 0;
let renderer!: FrameRenderer;
renderer = new FrameRenderer({
request(callback) {
callbacks.push(callback);
return () => { throw cancelError; };
},
}, () => {
rendered += 1;
renderer.request();
throw renderError;
}, error => { faults.push(error); });
renderer.request();
let thrown: unknown;
try {
callbacks[0]!();
} catch (error) {
thrown = error;
}
assert.equal(thrown, undefined);
assert.equal(faults.length, 1);
const fault = faults[0];
assert.ok(fault instanceof Error);
assert.ok('errors' in fault);
assert.deepEqual(fault.errors, [renderError, cancelError]);
callbacks[1]!();
assert.equal(rendered, 1);
assert.equal(faults.length, 1);
});
test('flush rethrows a synchronous render error without reporting an async fault', () => {
const callbacks: (() => void)[] = [];
const renderError = new Error('render failed');
const faults: unknown[] = [];
let rendered = 0;
const renderer = new FrameRenderer({
request(callback) { callbacks.push(callback); return () => {}; },
}, () => {
rendered += 1;
throw renderError;
}, error => { faults.push(error); });
renderer.request();
let thrown: unknown;
try {
renderer.flush();
} catch (error) {
thrown = error;
}
assert.equal(thrown, renderError);
assert.deepEqual(faults, []);
callbacks[0]!();
assert.equal(rendered, 1);
});
test('does not swallow an onFault callback error', () => {
const callbacks: (() => void)[] = [];
const renderError = new Error('render failed');
const callbackError = new Error('fault callback failed');
const reported: unknown[] = [];
const renderer = new FrameRenderer({
request(callback) { callbacks.push(callback); return () => {}; },
}, () => { throw renderError; }, error => {
reported.push(error);
throw callbackError;
});
renderer.request();
let thrown: unknown;
try {
callbacks[0]!();
} catch (error) {
thrown = error;
}
assert.equal(thrown, callbackError);
assert.deepEqual(reported, [renderError]);
renderer.request();
assert.equal(callbacks.length, 1);
});