generated from Labyricorn/labyricorn-project-template
Steps 6.4-6.7B — Local surfaces, reference-exhibit validation, SciFi Observation surface
One commit for the work accumulated in the working tree since Step 6.3, which had never been split into per-step commits: - src/local-surfaces.js + src/surface-url.js (new); src/ui.js, src/validation.js, src/connection.js and public/index.html updated for local-surface hosting and generic surface rendering - tests: local-surfaces (20), scifi-surfaces (24) and postmessage-interop (7) new; connection/museum-gallery/surface-validation suites updated - reference exhibits: shared/contract-core.js defaults to Contract 5.3 (major 5, minor 3, xzbt 5.3); museum-gallery advertises its surface catalog; aquarium/haunted-house/planetarium adapters updated - SciFi-XZBT (Step 6.7A/6.7B): surface-mode.js + surface-bus.js, Observation-surface boot branch, local-change hooks, view.pillars / view.warp-flight targets; fixture byte-identical to G:/.vibe/SciFi-XZBT - SciFi-XZBT contract adapter handshake fix: the inbound bridge filter no longer gates on an exact advisory xzbt value (Contract 5.3 §6.5), only on its presence/type, matching the host's own envelope validation; the adapter now advertises contract minor 3 / version 5.3.0, which it already implemented via the 5.3 surfaces field. Root cause of the five failing postmessage-interop tests (host hello was silently dropped). - docs: architecture 6.4 and 6.7A, reference 6.6 and 6.7; evidence logs; test-fixtures/PROVENANCE.md resync record Test results: NGN 154/154 (was 149/154); postmessage-interop 7/7 (was 2/7); SciFi contract harness 21/21, real-adapter suite 32/32. git diff --check clean for changed files; two pre-existing trailing-whitespace lines remain in test-fixtures/reference-exhibits/scifi/index.html, copied verbatim from the authoritative SciFi source. Step 6.7 live verification (browser Observation, packaged standalone) is still pending and is not claimed here.
This commit is contained in:
@@ -71,6 +71,16 @@
|
||||
link.mutate('set', 'artifact.selected', next);
|
||||
});
|
||||
|
||||
/* `beforeunload` is best-effort only -- it does not reliably fire when
|
||||
* NGN removes/replaces this frame's iframe element directly (as opposed
|
||||
* to a real navigation), so it is not the correctness mechanism here.
|
||||
* `__xzbtSurfaceDispose` is a well-known, optional hook: local-surfaces.js
|
||||
* calls it synchronously, if present, on this frame's contentWindow right
|
||||
* before removing the iframe -- deterministic because it is a direct
|
||||
* same-origin call, not a queued postMessage that a doomed frame might
|
||||
* never get to process. detach() is idempotent, so it is safe for both
|
||||
* this hook and beforeunload to fire (or neither, or either alone). */
|
||||
window.__xzbtSurfaceDispose = function () { link.detach(); };
|
||||
window.addEventListener('beforeunload', function () { link.detach(); });
|
||||
window.__museumGalleryDebug = { link: link };
|
||||
})();
|
||||
|
||||
@@ -126,11 +126,12 @@
|
||||
setters: setters,
|
||||
readers: readers,
|
||||
actions: actions,
|
||||
surfaces: surfaces,
|
||||
/* Contract 5.3 adoption is per-instance; every other reference exhibit
|
||||
* keeps the shared core's Contract 5.2 defaults untouched. */
|
||||
contractMinor: 3,
|
||||
xzbtVersion: '5.3'
|
||||
/* contractMinor/xzbtVersion are intentionally omitted: the shared
|
||||
* contract-core.js now defaults every exhibit to Contract 5.3, and
|
||||
* Museum Gallery has no reason to override that default. `surfaces`
|
||||
* is the one thing that makes this exhibit's contract usage different
|
||||
* from the others -- Contract 5.3 §31 presentation surfaces. */
|
||||
surfaces: surfaces
|
||||
});
|
||||
|
||||
return core;
|
||||
|
||||
@@ -81,12 +81,17 @@
|
||||
* surface) to attached surfaces, and answers their attach requests with a
|
||||
* fresh snapshot read straight from this Core -- never a second
|
||||
* computation. */
|
||||
var bus = window.MuseumGallerySurfaceBus.createOwner(core);
|
||||
function updateSubscriberCount(count) {
|
||||
Shell.setText(subscriberCountEl, String(count));
|
||||
}
|
||||
var bus = window.MuseumGallerySurfaceBus.createOwner(core, {
|
||||
onConnectionChange: updateSubscriberCount
|
||||
});
|
||||
updateSubscriberCount(bus.attachedCount());
|
||||
var priorOnEvent = core.onEvent;
|
||||
core.onEvent = function (event) {
|
||||
if (typeof priorOnEvent === 'function') priorOnEvent(event);
|
||||
sync();
|
||||
Shell.setText(subscriberCountEl, String(bus.attachedCount()));
|
||||
};
|
||||
|
||||
/* Optional NGN attachment -- same optional transport every reference
|
||||
|
||||
@@ -56,6 +56,12 @@
|
||||
link.mutate('set', 'labels.enabled', !latest['labels.enabled']);
|
||||
});
|
||||
|
||||
/* Same rationale as artifact.boot.js: `__xzbtSurfaceDispose` is the
|
||||
* deterministic path (called synchronously by local-surfaces.js right
|
||||
* before it removes this frame); `beforeunload` remains only as a
|
||||
* best-effort fallback for the cases NGN can't foresee (e.g. the tab
|
||||
* closing on its own). detach() is idempotent either way. */
|
||||
window.__xzbtSurfaceDispose = function () { link.detach(); };
|
||||
window.addEventListener('beforeunload', function () { link.detach(); });
|
||||
window.__museumGalleryDebug = { link: link };
|
||||
})();
|
||||
|
||||
@@ -39,23 +39,42 @@
|
||||
* second computation of state.
|
||||
*
|
||||
* @param {object} core an XZBTContractCore.ContractCore instance
|
||||
* @param {object} [options]
|
||||
* @param {function} [options.onConnectionChange] (attachedCount)
|
||||
*/
|
||||
function createOwner(core) {
|
||||
function createOwner(core, options) {
|
||||
options = options || {};
|
||||
var channel = new BroadcastChannel(CHANNEL_NAME);
|
||||
var attachedCount = 0;
|
||||
/* Participant bookkeeping is identity-based (a Set of live participant
|
||||
* ids), not a bare counter. This is what makes it idempotent: a
|
||||
* duplicate `attach` from a still-live participant cannot inflate the
|
||||
* count, and a `detach` for an id that is not (or no longer) present
|
||||
* cannot underflow it -- both matter because iframe reload/removal can
|
||||
* make attach/detach delivery imperfect (see attach()'s participantId
|
||||
* and local-surfaces.js's pre-removal dispose call). */
|
||||
var participants = new Set();
|
||||
|
||||
function notifyConnectionChange() {
|
||||
if (typeof options.onConnectionChange === 'function') {
|
||||
options.onConnectionChange(participants.size);
|
||||
}
|
||||
}
|
||||
|
||||
channel.onmessage = function (ev) {
|
||||
var msg = ev.data;
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
if (msg.type === 'attach') {
|
||||
attachedCount += 1;
|
||||
if (typeof msg.participantId === 'string') {
|
||||
participants.add(msg.participantId);
|
||||
}
|
||||
channel.postMessage({
|
||||
type: 'attach.snapshot',
|
||||
inReplyTo: msg.requestId,
|
||||
snapshot: core.stateSnapshot(),
|
||||
registryRevision: core.registryRevision
|
||||
});
|
||||
notifyConnectionChange();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,7 +94,13 @@
|
||||
}
|
||||
|
||||
if (msg.type === 'detach') {
|
||||
attachedCount = Math.max(0, attachedCount - 1);
|
||||
/* A detach with no/unknown participantId (a stray legacy message, a
|
||||
* duplicate, or one that arrives after that id was already removed)
|
||||
* is a no-op -- Set.delete() only reports and fires the callback
|
||||
* when it actually removed something live. */
|
||||
if (typeof msg.participantId === 'string' && participants.delete(msg.participantId)) {
|
||||
notifyConnectionChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,7 +112,7 @@
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
attachedCount: function () { return attachedCount; },
|
||||
attachedCount: function () { return participants.size; },
|
||||
close: function () { channel.close(); }
|
||||
};
|
||||
}
|
||||
@@ -109,7 +134,14 @@
|
||||
options = options || {};
|
||||
var channel = new BroadcastChannel(CHANNEL_NAME);
|
||||
var requestId = randomId();
|
||||
/* A stable identity for THIS attachment (this document load, this
|
||||
* BroadcastChannel instance). A reload creates a brand-new attach() call
|
||||
* and therefore a brand-new participantId, which is exactly what lets
|
||||
* the owner's Set-based bookkeeping retire the old identity and adopt
|
||||
* the new one without the two ever being confused for each other. */
|
||||
var participantId = randomId();
|
||||
var attached = false;
|
||||
var detached = false;
|
||||
|
||||
var timer = setTimeout(function () {
|
||||
if (attached) return;
|
||||
@@ -134,7 +166,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
channel.postMessage({ type: 'attach', requestId: requestId });
|
||||
channel.postMessage({ type: 'attach', requestId: requestId, participantId: participantId });
|
||||
|
||||
function detach() {
|
||||
/* Idempotent: local-surfaces.js's deterministic pre-removal dispose
|
||||
* call and this document's own best-effort `beforeunload` handler can
|
||||
* both end up calling detach() for the same attachment. Only the
|
||||
* first actually posts a message; every call still tears down the
|
||||
* timer/channel so repeated calls are always safe. */
|
||||
if (attached && !detached) {
|
||||
detached = true;
|
||||
channel.postMessage({ type: 'detach', participantId: participantId });
|
||||
}
|
||||
clearTimeout(timer);
|
||||
channel.close();
|
||||
}
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
@@ -148,11 +194,7 @@
|
||||
channel.postMessage(payload);
|
||||
return true;
|
||||
},
|
||||
detach: function () {
|
||||
if (attached) channel.postMessage({ type: 'detach' });
|
||||
clearTimeout(timer);
|
||||
channel.close();
|
||||
}
|
||||
detach: detach
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user