generated from Labyricorn/labyricorn-project-template
Step 6.2 Complete — Museum Gallery reference exhibit and Contract 5.3 spec
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Museum Gallery — exhibit-internal surface-attachment bus.
|
||||
*
|
||||
* This implements the generic attachment sequence from the Step 6.1
|
||||
* architecture document (docs/architecture/XZBT-Multi-Surface-Model-Step6.1.md
|
||||
* §7.2) and Contract 5.3 §31.7's "cross-document attachment" transport
|
||||
* shape: BroadcastChannel, same-origin, with a clearly identified
|
||||
* authoritative owner document (the primary surface, control.html).
|
||||
*
|
||||
* This file is NOT part of the XZBT Exhibit Contract wire protocol. It
|
||||
* carries no contract envelope, is never observed by a host, and its
|
||||
* messages are exhibit-internal only. An exhibit is free to choose a
|
||||
* different transport (SharedWorker, in-process) — this reference exhibit's
|
||||
* choice of BroadcastChannel is documented in README.md, not in Contract 5.3.
|
||||
*
|
||||
* The sequence (Step 6.1 §7.2, steps 1-5):
|
||||
* 1. surface requests attachment
|
||||
* 2. owner replies with current snapshot + stateRevision
|
||||
* 3. owner registers the surface as a subscriber for ongoing events
|
||||
* 4. native interactions on the surface are posted back to the owner,
|
||||
* which routes them through the Core's one canonical mutation path
|
||||
* 5. on detach, the owner simply stops delivering to that channel
|
||||
* instance; no state is held by the surface, so nothing reconciles.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var CHANNEL_NAME = 'xzbt-museum-gallery-core-v1';
|
||||
|
||||
function randomId() {
|
||||
return 'req-' + Math.random().toString(16).slice(2) + Date.now().toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner side. Call once, in the document that constructs the Exhibit
|
||||
* State Core (control.html). Wraps `core.onEvent` to forward every
|
||||
* normalized event to attached surfaces, and answers attach/mutate
|
||||
* requests by calling straight into the Core's existing methods — never a
|
||||
* second computation of state.
|
||||
*
|
||||
* @param {object} core an XZBTContractCore.ContractCore instance
|
||||
*/
|
||||
function createOwner(core) {
|
||||
var channel = new BroadcastChannel(CHANNEL_NAME);
|
||||
var attachedCount = 0;
|
||||
|
||||
channel.onmessage = function (ev) {
|
||||
var msg = ev.data;
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
if (msg.type === 'attach') {
|
||||
attachedCount += 1;
|
||||
channel.postMessage({
|
||||
type: 'attach.snapshot',
|
||||
inReplyTo: msg.requestId,
|
||||
snapshot: core.stateSnapshot(),
|
||||
registryRevision: core.registryRevision
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'mutate') {
|
||||
/* Every surface-originated interaction that changes contract-visible
|
||||
* state or executes a contract-visible action goes through exactly
|
||||
* the same core.applyMutation/invokeAction chokepoint a contract
|
||||
* set/invoke from NGN would use (Contract 5.3 §31.7). Source is
|
||||
* always 'ui': a surface interaction is exhibit-native UI regardless
|
||||
* of which surface it originated on (Contract 5.3 §15). */
|
||||
if (msg.kind === 'set' && typeof msg.target === 'string') {
|
||||
core.applyMutation(msg.target, msg.value, 'ui');
|
||||
} else if (msg.kind === 'invoke' && typeof msg.target === 'string') {
|
||||
core.invokeAction(msg.target, msg.args || {}, 'ui');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'detach') {
|
||||
attachedCount = Math.max(0, attachedCount - 1);
|
||||
}
|
||||
};
|
||||
|
||||
var priorOnEvent = core.onEvent;
|
||||
core.onEvent = function (event) {
|
||||
if (typeof priorOnEvent === 'function') priorOnEvent(event);
|
||||
channel.postMessage({ type: 'core-event', event: event });
|
||||
};
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
attachedCount: function () { return attachedCount; },
|
||||
close: function () { channel.close(); }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface side. Call from any non-primary surface document. Requests
|
||||
* attachment and waits `timeoutMs` for a reply; if the owner document
|
||||
* (control.html) is not open, `onTimeout` fires and the surface must
|
||||
* degrade to a clear waiting/disconnected state rather than inventing its
|
||||
* own Core (Contract 5.3 §31.8, Step 6.1 §7.2).
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {number} [options.timeoutMs]
|
||||
* @param {function} [options.onSnapshot] (snapshot, registryRevision)
|
||||
* @param {function} [options.onEvent] (normalizedEvent)
|
||||
* @param {function} [options.onTimeout]
|
||||
*/
|
||||
function attach(options) {
|
||||
options = options || {};
|
||||
var channel = new BroadcastChannel(CHANNEL_NAME);
|
||||
var requestId = randomId();
|
||||
var attached = false;
|
||||
|
||||
var timer = setTimeout(function () {
|
||||
if (attached) return;
|
||||
if (typeof options.onTimeout === 'function') options.onTimeout();
|
||||
}, options.timeoutMs || 1000);
|
||||
|
||||
channel.onmessage = function (ev) {
|
||||
var msg = ev.data;
|
||||
if (!msg || typeof msg !== 'object') return;
|
||||
|
||||
if (msg.type === 'attach.snapshot' && msg.inReplyTo === requestId && !attached) {
|
||||
attached = true;
|
||||
clearTimeout(timer);
|
||||
if (typeof options.onSnapshot === 'function') {
|
||||
options.onSnapshot(msg.snapshot, msg.registryRevision);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'core-event' && attached) {
|
||||
if (typeof options.onEvent === 'function') options.onEvent(msg.event);
|
||||
}
|
||||
};
|
||||
|
||||
channel.postMessage({ type: 'attach', requestId: requestId });
|
||||
|
||||
return {
|
||||
channel: channel,
|
||||
isAttached: function () { return attached; },
|
||||
/** Route a native interaction through the owner's canonical mutation path. */
|
||||
mutate: function (kind, target, valueOrArgs) {
|
||||
if (!attached) return false;
|
||||
var payload = { type: 'mutate', kind: kind, target: target };
|
||||
if (kind === 'set') payload.value = valueOrArgs;
|
||||
else payload.args = valueOrArgs || {};
|
||||
channel.postMessage(payload);
|
||||
return true;
|
||||
},
|
||||
detach: function () {
|
||||
if (attached) channel.postMessage({ type: 'detach' });
|
||||
clearTimeout(timer);
|
||||
channel.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
window.MuseumGallerySurfaceBus = {
|
||||
CHANNEL_NAME: CHANNEL_NAME,
|
||||
createOwner: createOwner,
|
||||
attach: attach
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user