@plotinus/matrix-package-observable-coordinator
Version:
Observable coordinator pattern components using IntrospectableBaseCommunicationComponent and proper presentation architecture
558 lines (527 loc) • 24 kB
JavaScript
// dist/browser/component-sources.js
var componentSources = {
// App component sources
appCommunication: `class AppComponent extends window.Matrix.BaseCommunicationComponent {
static dslTag = 'app';
static isMatrixComponent = true;
startExecution() {
console.log(\`App \${this.id} started\`);
this.setState({ status: 'ready' });
}
handleStartProcessing() {
console.log('App: Starting processing');
this.sendCommand('myCoordinator', 'StartProcessing', { jobCount: 5 });
}
handleCoordinatorReady() {
console.log('App: Coordinator is ready, sending StartProcessing command');
this.sendCommand('myCoordinator', 'StartProcessing', { jobCount: 5 });
}
handleCoordinatorDone(data) {
console.log('App: All done!', data);
this.setState({ completedJobs: data.completedJobIds });
}
}`,
appPresentation: `class AppPresentationElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
bindTo(commComponent) {
this.commComponent = commComponent;
this.render();
}
render() {
this.shadowRoot.innerHTML = \`
<style>
:host { display: block; }
.app { border: 2px solid #8b5cf6; padding: 16px; border-radius: 8px; }
button { background: #8b5cf6; color: white; border: none; padding: 8px 16px;
border-radius: 6px; cursor: pointer; }
</style>
<div class="app">
<h3>App: \${this.commComponent?.id}</h3>
<button onclick="this.getRootNode().host.startProcessing()">Start Processing</button>
<slot></slot>
</div>
\`;
}
startProcessing() {
this.commComponent?.handleStartProcessing();
}
}`,
// Coordinator component sources
coordinatorCommunication: `class CoordinatorComponent extends window.Matrix.BaseCommunicationComponent {
static dslTag = 'coordinator';
static isMatrixComponent = true;
constructor(id, eventBus) {
super(id, eventBus);
this.workers = new Set();
this.completedJobs = new Set();
this.jobQueue = [];
}
startExecution() {
this.setState({ status: 'ready' });
this.emitEvent('CoordinatorReady');
}
startProcessingHandler(data) {
console.log('Coordinator: Starting processing');
console.log('Coordinator: Creating jobs');
for (let i = 0; i < (data.jobCount || 5); i++) {
this.jobQueue.push({ id: \`job-\${i + 1}\` });
}
this.distributeJobs();
}
distributeJobs() {
this.workers.forEach(workerId => {
if (this.jobQueue.length > 0) {
const job = this.jobQueue.shift();
this.sendCommand(workerId, 'ProcessJob', job);
}
});
}
handleWorkerRegistered(data) {
this.workers.add(data.workerId);
this.setState({ workerCount: this.workers.size });
}
handleWorkerJobCompleted(data) {
this.completedJobs.add(data.jobId);
if (this.jobQueue.length > 0) {
const job = this.jobQueue.shift();
this.sendCommand(data.workerId, 'ProcessJob', job);
} else if (this.completedJobs.size >= 5) {
this.emitEvent('CoordinatorDone', {
completedJobIds: Array.from(this.completedJobs)
});
}
}
}`,
coordinatorPresentation: `class CoordinatorPresentationElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
bindTo(commComponent) {
this.commComponent = commComponent;
// Subscribe to state changes
if (commComponent.eventBus) {
const stateKey = \`cmp:\${commComponent.id}:_stateChanged\`;
commComponent.eventBus.on(stateKey, () => this.render());
}
this.render();
}
render() {
const state = this.commComponent?.state || {};
this.shadowRoot.innerHTML = \`
<style>
:host { display: block; }
.coordinator { border: 2px solid #58a6ff; padding: 16px; margin: 8px; border-radius: 8px; }
</style>
<div class="coordinator">
<h3>Coordinator: \${this.commComponent?.id}</h3>
<div>Workers: \${state.workerCount || 0}</div>
<slot></slot>
</div>
\`;
}
}`,
// Worker component sources
workerCommunication: `class WorkerComponent extends window.Matrix.BaseCommunicationComponent {
static dslTag = 'worker';
static isMatrixComponent = true;
startExecution() {
this.setState({ jobsProcessed: 0 });
this.emitEvent('Registered', { workerId: this.id });
}
processJobHandler(job) {
console.log(\`Worker \${this.id}: Processing \${job.id}\`);
setTimeout(() => {
this.setState({ jobsProcessed: (this.state.jobsProcessed || 0) + 1 });
this.emitEvent('JobCompleted', {
workerId: this.id,
jobId: job.id
});
}, 1000);
}
}`,
workerPresentation: `class WorkerPresentationElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
bindTo(commComponent) {
this.commComponent = commComponent;
// Subscribe to state changes
if (commComponent.eventBus) {
const stateKey = \`cmp:\${commComponent.id}:_stateChanged\`;
commComponent.eventBus.on(stateKey, () => this.render());
}
this.render();
}
render() {
const state = this.commComponent?.state || {};
this.shadowRoot.innerHTML = \`
<style>
:host { display: block; }
.worker { border: 2px solid #3fb950; padding: 12px; margin: 8px; border-radius: 6px; }
</style>
<div class="worker">
<h4>Worker: \${this.commComponent?.id}</h4>
<div>Jobs: \${state.jobsProcessed || 0}</div>
</div>
\`;
}
}`
};
// dist/presentation/app-presentation.js
var AppPresentationElement = class extends HTMLElement {
/**
* 🏗️ Element Construction - Setting Up Presentation Infrastructure
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* Initializes the custom element with Shadow DOM and presentation-specific state.
* This demonstrates clean separation between presentation state (logMessages)
* and communication component state (managed by AppComponent).
*/
constructor() {
super();
this.logMessages = [];
this.logMessages = [];
console.log("\u{1F3D7}\uFE0F AppPresentationElement: Constructor called");
this.attachShadow({ mode: "open" });
this.logToUI("\u{1F3D7}\uFE0F AppPresentation element constructed");
}
/**
* 🔗 Component Binding - The Core Introspection Connection
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* This method establishes the CRITICAL connection between the presentation layer and the
* communication component's introspection system. It subscribes to three key introspection
* events that enable real-time UI updates based on observable state changes.
*
* INTROSPECTION EVENTS SUBSCRIBED:
* • _stateChanged: Full state object updates (triggers complete UI re-render)
* • _propertyChanged: Individual property updates (triggers targeted UI updates)
* • _eventEmitted: Component event emissions (for debugging and system transparency)
*/
bindTo(commComponent) {
console.log(`\u{1F517} AppPresentation: bindTo() called with:`, commComponent?.id || "undefined");
this.logToUI(`\u{1F517} bindTo() called with: ${commComponent?.id || "undefined"}`);
this.commComponent = commComponent;
if (commComponent.eventBus) {
console.log(`\u{1F442} AppPresentation: Setting up event listeners for ${commComponent.id}`);
commComponent.eventBus.on(`cmp:${commComponent.id}:_stateChanged`, (data) => {
console.log(`\u{1F3AF} AppPresentation: Received _stateChanged event:`, data);
this.onStateChanged(data.state);
});
commComponent.eventBus.on(`cmp:${commComponent.id}:_propertyChanged`, (data) => {
console.log(`\u{1F3AF} AppPresentation: Received _propertyChanged event:`, data);
this.onPropertyChanged(data.propertyName, data.newValue, data.oldValue);
});
commComponent.eventBus.on(`cmp:${commComponent.id}:_eventEmitted`, (data) => {
console.log(`\u{1F3AF} AppPresentation: Received _eventEmitted event:`, data);
});
console.log(`\u2705 AppPresentation: Event listeners registered for ${commComponent.id}`);
} else {
console.warn(`\u26A0\uFE0F AppPresentation: No eventBus found on communication component!`);
}
this.render();
}
/**
* 🎨 UI Rendering - State-Driven Dynamic Display
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* This method demonstrates REACTIVE UI PATTERNS where the presentation layer
* automatically updates based on observable state changes from the communication component.
* It transforms abstract state data into visual indicators and user-friendly status displays.
*
* RENDERING PATTERNS DEMONSTRATED:
* • State-to-UI Mapping: Convert boolean/array state to visual status indicators
* • Conditional UI Elements: Show/hide elements based on state values
* • Real-time Data Binding: Automatic UI updates when state changes via introspection
* • Presentation State Integration: Combine communication state with UI-specific data
*/
render() {
console.log(`\u{1F3A8} AppPresentation: Rendering with current state:`, this.commComponent?.state);
const state = this.commComponent?.state || {};
const coordinatorStatus = state.coordinatorReady ? "\u2705 Ready" : "\u23F3 Waiting";
const processingStatus = state.processingStarted ? "\u{1F504} Processing" : "\u23F8\uFE0F Idle";
const completionStatus = state.systemCompleted ? "\u2705 Complete" : "\u{1F504} In Progress";
const jobCount = state.completedJobs?.length || 0;
this.shadowRoot.innerHTML = `
<style>
/* \u{1F3A8} COMPONENT STYLING: Encapsulated styles for presentation layer */
:host { display: block; }
.app {
border: 2px solid #8b5cf6;
padding: 16px;
border-radius: 8px;
background: #1f2937;
color: #e5e7eb;
}
.app h3 { color: #8b5cf6; margin-top: 0; }
.status { margin: 8px 0; padding: 8px; background: #374151; border-radius: 4px; }
.status-item { margin: 4px 0; }
.logger {
background: #111827;
color: #10b981;
padding: 8px;
margin: 8px 0;
border-radius: 4px;
font-family: monospace;
font-size: 11px;
max-height: 150px;
overflow-y: auto;
border: 1px solid #374151;
}
.log-entry { margin: 2px 0; word-break: break-all; }
button {
background: #8b5cf6;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
margin: 4px;
transition: background-color 0.2s;
}
button:hover { background: #7c3aed; }
.jobs { font-family: monospace; font-size: 12px; color: #fbbf24; }
</style>
<div class="app">
<h3>\u{1F3AF} Observable App: ${this.commComponent?.id}</h3>
<!-- \u{1F4CA} REAL-TIME STATUS DISPLAY: Observable state visualized as status indicators -->
<div class="status">
<div class="status-item"><strong>Coordinator:</strong> ${coordinatorStatus}</div>
<div class="status-item"><strong>Processing:</strong> ${processingStatus}</div>
<div class="status-item"><strong>System:</strong> ${completionStatus}</div>
<div class="status-item"><strong>Completed Jobs:</strong> ${jobCount}</div>
${state.completedJobs?.length ? `<div class="jobs">Jobs: ${state.completedJobs.join(", ")}</div>` : ""}
</div>
<!-- \u{1F4CB} INTROSPECTION LOG: Real-time event and state change tracking -->
<div class="logger">
<div style="color: #fbbf24; font-weight: bold;">\u{1F4CB} APP PRESENTATION LOG:</div>
${this.logMessages.map((msg) => `<div class="log-entry">${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${msg}</div>`).join("")}
</div>
<!-- \u{1F680} UI INTERACTION: Button triggers communication component method -->
<button onclick="this.getRootNode().host.startProcessing()">\u{1F680} Start Processing</button>
<slot></slot>
</div>
`;
}
/**
* 🚀 User Interaction Handler - UI-to-Communication Bridge
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* This method demonstrates the TWO-WAY COMMUNICATION pattern where UI interactions
* trigger communication component methods. The presentation layer acts as a bridge
* between user actions and the observable communication layer.
*
* INTERACTION FLOW:
* Button click → startProcessing() → commComponent.handleStartProcessing() → setState() → introspection events → UI updates
*/
startProcessing() {
console.log(`\u{1F680} AppPresentation: Start processing button clicked`);
this.logToUI("\u{1F680} Button clicked!");
this.commComponent?.handleStartProcessing();
}
/**
* 📋 Presentation Logging - UI-Specific State Management
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* This method demonstrates PRESENTATION-SPECIFIC STATE management separate from
* communication component observable state. It maintains UI-only data (log messages)
* and provides targeted DOM updates to avoid infinite render loops.
*
* PRESENTATION STATE PATTERNS:
* • UI-Only Data: logMessages array is presentation-specific, not part of communication state
* • Targeted Updates: Update only the logger section to avoid triggering full re-renders
* • Circular Reference Prevention: Careful not to call this from render() method
*/
logToUI(message) {
this.logMessages.push(message);
if (this.logMessages.length > 20)
this.logMessages.shift();
if (this.shadowRoot?.innerHTML) {
const loggerDiv = this.shadowRoot.querySelector(".logger");
if (loggerDiv) {
loggerDiv.innerHTML = `
<div style="color: #fbbf24; font-weight: bold;">\u{1F4CB} APP PRESENTATION LOG:</div>
${this.logMessages.map((msg) => `<div class="log-entry">${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${msg}</div>`).join("")}
`;
}
}
}
/**
* 🔄 State Change Handler - Observable State Response
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* This method is called automatically when the communication component's setState() method
* is called, demonstrating the REACTIVE UI PATTERN core to the Matrix Framework's
* introspection system. It shows how presentation layers stay synchronized with business logic.
*
* INTROSPECTION EVENT FLOW:
* setState() in AppComponent → _stateChanged event → this.onStateChanged() → render() → UI updates
*/
onStateChanged(newState) {
console.log(`\u{1F504} AppPresentation: State changed, re-rendering with state:`, newState);
this.logToUI(`\u{1F504} State changed: ${JSON.stringify(newState)}`);
this.render();
}
/**
* 📝 Property Change Handler - Granular State Tracking
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* This method provides GRANULAR CHANGE TRACKING for individual properties within the
* observable state. It's called for each property that changes when setState() is called,
* enabling detailed monitoring and potentially targeted UI updates.
*
* GRANULAR INTROSPECTION PATTERN:
* setState({prop1: val1, prop2: val2}) → triggers onPropertyChanged() twice → individual property tracking
*/
onPropertyChanged(propertyName, newValue, oldValue) {
console.log(`\u{1F4DD} AppPresentation: Property ${propertyName} changed from ${oldValue} to ${newValue}, re-rendering`);
this.logToUI(`\u{1F4DD} Property ${propertyName}: ${oldValue} \u2192 ${newValue}`);
this.render();
}
/**
* 🔌 DOM Connection Lifecycle - Web Component Integration
* ══════════════════════════════════════════════════════════════════════════════════════════════════
*
* This Web Components lifecycle method is called when the element is added to the DOM.
* It demonstrates the integration between Matrix Framework components and standard
* Web Components lifecycle, ensuring proper initialization and rendering.
*
* WEB COMPONENT LIFECYCLE INTEGRATION:
* Element creation → constructor() → bindTo() → connectedCallback() → DOM ready
*/
connectedCallback() {
console.log("\u{1F50C} AppPresentationElement: Connected to DOM");
this.logToUI("\u{1F50C} Connected to DOM");
this.render();
}
};
// dist/presentation/coordinator-presentation.js
var CoordinatorPresentationElement = class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
bindTo(commComponent) {
this.commComponent = commComponent;
if (commComponent.eventBus) {
const stateKey = `cmp:${commComponent.id}:_stateChanged`;
commComponent.eventBus.on(stateKey, () => this.render());
}
this.render();
}
render() {
const state = this.commComponent?.state || {};
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
.coordinator { border: 2px solid #58a6ff; padding: 16px; margin: 8px; border-radius: 8px; }
</style>
<div class="coordinator">
<h3>Coordinator: ${this.commComponent?.id}</h3>
<div>Workers: ${state.workerCount || 0}</div>
<slot></slot>
</div>
`;
}
};
// dist/presentation/worker-presentation.js
var WorkerPresentationElement = class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
bindTo(commComponent) {
this.commComponent = commComponent;
if (commComponent.eventBus) {
const stateKey = `cmp:${commComponent.id}:_stateChanged`;
commComponent.eventBus.on(stateKey, () => this.render());
}
this.render();
}
render() {
const state = this.commComponent?.state || {};
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
.worker { border: 2px solid #3fb950; padding: 12px; margin: 8px; border-radius: 6px; }
</style>
<div class="worker">
<h4>Worker: ${this.commComponent?.id}</h4>
<div>Jobs: ${state.jobsProcessed || 0}</div>
</div>
`;
}
};
// dist/browser-entry.js
function registerObservableCoordinatorComponents(Matrix) {
if (!Matrix || !Matrix.BaseCommunicationComponent) {
throw new Error("Matrix framework not found");
}
const components = {
app: {
name: "AppComponent",
source: componentSources.appCommunication,
tag: "app"
},
coordinator: {
name: "CoordinatorComponent",
source: componentSources.coordinatorCommunication,
tag: "coordinator"
},
worker: {
name: "WorkerComponent",
source: componentSources.workerCommunication,
tag: "worker"
}
};
Object.values(components).forEach((comp) => {
try {
const ComponentClass = new Function(
"BaseCommunicationComponent",
comp.source + "\nreturn " + comp.name
)(Matrix.BaseCommunicationComponent);
ComponentClass.dslTag = comp.tag;
ComponentClass.isMatrixComponent = true;
if (!window.MatrixComponents) {
window.MatrixComponents = {};
}
window.MatrixComponents[comp.name] = ComponentClass;
Matrix.register(ComponentClass);
console.log("Registered communication component:", comp.tag);
} catch (error) {
console.error("Failed to register component:", comp.name, error);
}
});
if (!customElements.get("app-presentation")) {
customElements.define("app-presentation", AppPresentationElement);
}
if (!customElements.get("coordinator-presentation")) {
customElements.define("coordinator-presentation", CoordinatorPresentationElement);
}
if (!customElements.get("worker-presentation")) {
customElements.define("worker-presentation", WorkerPresentationElement);
}
console.log("All observable coordinator components registered");
}
if (typeof window !== "undefined") {
window.MatrixPackageObservableCoordinator = {
componentSources,
AppPresentationElement,
CoordinatorPresentationElement,
WorkerPresentationElement,
registerObservableCoordinatorComponents
};
}
export {
AppPresentationElement,
CoordinatorPresentationElement,
WorkerPresentationElement,
componentSources,
registerObservableCoordinatorComponents
};
//# sourceMappingURL=browser-bundle.js.map