feat(presentation): add cancellable frame renderer
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user