feat(runtime): implement phases 1 and 2

This commit is contained in:
2026-09-05 12:59:15 -07:00
parent 75b4da0df4
commit c010e415a2
27 changed files with 4172 additions and 5 deletions
+112
View File
@@ -0,0 +1,112 @@
import { ActionExecutor } from './actions.js';
import { SeededRNG } from './rng.js';
import { ResolutionEngine } from './resolution.js';
export class CommonGrammarPerformance {
constructor(record, rootSeed, options = {}) {
this.record = record;
this.rootSeed = rootSeed;
this.rng = new SeededRNG(rootSeed);
this.diagnostics = options.diagnostics;
this.onUpdate = options.onUpdate ?? (() => {});
this.engine = new ResolutionEngine(record.document, this.rng, {
diagnostics: this.diagnostics,
initialParameters: options.initialParameters,
onParameterChange: options.onParameterChange
});
this.actions = new ActionExecutor(this.engine, { diagnostics: this.diagnostics });
this.state = 'prepared';
this.resources = new Set();
this.frameRequest = null;
this.lastFrame = undefined;
this.accumulator = 0;
this.boundFrame = (now) => this.frame(now);
this.boundPointer = (event) => {
this.engine.signals.set('signals.pointer.x', event.clientX);
this.engine.signals.set('signals.pointer.y', event.clientY);
this.engine.invalidate();
};
this.boundResize = () => {
this.engine.signals.set('signals.viewport.width', globalThis.innerWidth ?? 0);
this.engine.signals.set('signals.viewport.height', globalThis.innerHeight ?? 0);
this.engine.invalidate();
};
}
async activate() {
if (this.state !== 'prepared') throw new Error(`Cannot activate a performance in state ${this.state}.`);
this.state = 'active';
this.onUpdate(this.engine);
if (typeof globalThis.addEventListener === 'function') {
globalThis.addEventListener('pointermove', this.boundPointer, { passive: true });
globalThis.addEventListener('resize', this.boundResize, { passive: true });
}
if (typeof globalThis.requestAnimationFrame === 'function') this.frameRequest = globalThis.requestAnimationFrame(this.boundFrame);
}
frame(now) {
if (this.state !== 'active') return;
if (this.lastFrame === undefined) this.lastFrame = now;
const observed = Math.max(0, now - this.lastFrame);
this.lastFrame = now;
const accepted = Math.min(observed, 250);
this.accumulator += accepted;
const step = 1000 / 60;
let ticks = 0;
while (this.accumulator + 1e-9 >= step && ticks < 8) {
this.engine.advance(step);
this.accumulator -= step;
ticks += 1;
}
if (observed > 250 || this.accumulator >= step) {
this.accumulator = Math.min(this.accumulator, step - 1e-9);
this.diagnostics?.warn('WARN_CLOCK_STALL', 'Discarded excess elapsed time to preserve the fixed-step work bound.', { exhibitId: this.record.id, section: 'scheduler' });
}
if (ticks > 0) this.onUpdate(this.engine);
this.frameRequest = globalThis.requestAnimationFrame(this.boundFrame);
}
setParameter(id, value) {
const result = this.engine.setParameter(id, value);
this.onUpdate(this.engine);
return result;
}
execute(actionArray, context) {
const result = this.actions.execute(actionArray, context);
this.onUpdate(this.engine);
return result;
}
releaseOverride(id) {
const released = this.engine.overrides.beginRelease(id);
this.engine.resolveAll();
this.onUpdate(this.engine);
return released;
}
async deactivate() {
if (this.frameRequest !== null && typeof globalThis.cancelAnimationFrame === 'function') globalThis.cancelAnimationFrame(this.frameRequest);
this.frameRequest = null;
this.lastFrame = undefined;
if (typeof globalThis.removeEventListener === 'function') {
globalThis.removeEventListener('pointermove', this.boundPointer);
globalThis.removeEventListener('resize', this.boundResize);
}
if (this.state === 'active') this.state = 'inactive';
this.engine.overrides.clear();
this.resources.clear();
}
async dispose() {
if (this.frameRequest !== null && typeof globalThis.cancelAnimationFrame === 'function') globalThis.cancelAnimationFrame(this.frameRequest);
this.frameRequest = null;
if (typeof globalThis.removeEventListener === 'function') {
globalThis.removeEventListener('pointermove', this.boundPointer);
globalThis.removeEventListener('resize', this.boundResize);
}
this.engine.dispose();
this.resources.clear();
this.state = 'disposed';
}
}