generated from Labyricorn/labyricorn-project-template
- Implemented §31.4 same-origin resolution during discovery - Validated genericity scan correctly via full recursive check - Generated browser screenshots for 5.2/5.3 verification
50 lines
2.1 KiB
JavaScript
50 lines
2.1 KiB
JavaScript
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), this.url); }
|
|
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);
|
|
}
|
|
}
|