Step 5 Complete

This commit is contained in:
2026-09-14 13:01:45 -07:00
parent 76928f7870
commit 961919e017
34 changed files with 3309 additions and 273 deletions
+49
View File
@@ -0,0 +1,49 @@
import { ProtocolError } from './validation.js';
export function exhibitURL(value, base) {
if (!value.trim()) throw new Error('Enter an exhibit URL or served path.');
const url = new URL(value.trim(), base);
if (!['http:', 'https:'].includes(url.protocol) || url.origin !== new URL(base).origin || url.username || url.password) {
throw new Error('Enter a same-origin HTTP exhibit URL or served path.');
}
return url.href;
}
// Frame lifecycle only; protocol negotiation remains in ExhibitHost.
export class ExhibitConnection {
constructor({ host, createFrame, transport, base, changed = () => {}, loadTimeoutMs = 15000 }) {
Object.assign(this, { host, createFrame, transport, base, changed, loadTimeoutMs });
this.frame = null; this.url = ''; this.loading = false; this.generation = 0;
}
disconnect() {
this.generation++; clearTimeout(this.timer); this.loading = false;
this.frame?.remove(); this.frame = null; this.host.disconnect(); this.changed();
}
load(value) {
const url = exhibitURL(value, this.base); // Invalid input preserves the active exhibit.
this.disconnect(); this.url = url; this.loading = true;
const generation = this.generation;
const frame = this.createFrame(); this.frame = frame;
frame.addEventListener('load', () => {
if (generation !== this.generation) return;
clearTimeout(this.timer); this.loading = false; this.attach();
});
this.timer = setTimeout(() => {
if (generation !== this.generation) return;
this.disconnect();
this.host.report(new ProtocolError('LOAD_TIMEOUT', 'Exhibit did not finish loading. Check the served path and reconnect.'));
}, this.loadTimeoutMs);
frame.src = url; this.changed();
}
async attach() {
if (!this.frame || this.loading) return;
const generation = this.generation;
try { await this.host.connect(this.transport(this.frame)); }
catch (error) { if (generation === this.generation && !error.code) this.host.report(error); }
this.changed();
}
reconnect() {
if (this.frame && !this.loading) return this.attach();
if (this.url) this.load(this.url);
}
}