Cross-Library Integration

How Singularity integrates with Space-Map and Editor

Cross-Library Integration

The Singularity library integrates seamlessly with other parts of the GoGrab Space platform through dedicated integration services. This document explains how to leverage these integrations.

Architecture Overview

┌──────────────────────────────────────────┐
│         space-map-lib                     │
│  (Universe Management)                    │
│  • Create/manage universes                │
│  • Organize galaxies and worlds           │
└──────────────▲───────────────────────────┘
               │
               │ SpaceMapIntegrationService
               │ (Logic statistics & navigation)
               │
┌──────────────────────────────────────────┐
│         singularity-lib                   │
│  (Logic Building & Execution)             │
│  • Build logic flows                      │
│  • Compose flows                          │
│  • Execute logic                          │
└──────────────▼───────────────────────────┘
               │
               │ EditorIntegrationService
               │ (Widget event bindings)
               │
┌──────────────────────────────────────────┐
│         editor-web-lib                    │
│  (Page & Widget Management)               │
│  • Create pages                           │
│  • Design widgets                         │
└──────────────────────────────────────────┘

Space-Map Integration

Purpose

The SpaceMapIntegrationService allows space-map components to:

Getting Logic Statistics

import { SpaceMapIntegrationService } from '@libs-structural/singularity-lib';

export class UniverseDetailComponent {
  private spaceMapIntegration = inject(SpaceMapIntegrationService);
  
  async loadUniverseLogicStats(universeId: string) {
    const stats = await this.spaceMapIntegration.getUniverseLogicStats(universeId);
    
    console.log(`Universe has ${stats.flowCount} flows`);
    console.log(`Universe has ${stats.compositionCount} compositions`);
    console.log(`Universe has ${stats.blockCount} total blocks`);
  }
}

Observable Statistics

For reactive UI updates:

// Subscribe to stats changes
this.spaceMapIntegration
  .getUniverseLogicStats$(universeId)
  .subscribe(stats => {
    if (stats) {
      this.logicStats.set(stats);
    }
  });

Navigation

// Navigate from universe detail to logic builder
navigateToLogicBuilder(universeId: string) {
  const url = this.spaceMapIntegration.getLogicBuilderUrl(universeId);
  this.router.navigateByUrl(url);
  // Navigates to: /singularity/logic-builder?universeId=xxx
}

// Navigate to flow composer
navigateToFlowComposer(universeId: string) {
  const url = this.spaceMapIntegration.getFlowComposerUrl(universeId);
  this.router.navigateByUrl(url);
  // Navigates to: /singularity/flow-composer?universeId=xxx
}

Checking for Logic

// Check if universe has any logic before showing options
async checkLogic(universeId: string) {
  const hasLogic = await this.spaceMapIntegration.hasLogic(universeId);
  
  if (hasLogic) {
    this.showLogicBuilderOption.set(true);
  }
}

Cache Management

// Refresh statistics after creating/deleting flows
async onFlowCreated(universeId: string) {
  await this.spaceMapIntegration.refreshUniverseStats(universeId);
  // Stats are recalculated
}

// Clear all cached statistics
this.spaceMapIntegration.clearCache();

Complete Example: Universe Card

import { Component, input, signal, inject, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { SpaceMapIntegrationService, UniverseLogicMetadata } from '@libs-structural/singularity-lib';

@Component({
  selector: 'app-universe-card',
  template: `
    <div class="universe-card">
      <h3>{{ universe().name }}</h3>
      
      @if (logicStats()) {
        <div class="stats">
          <div class="stat">
            <span class="label">Flows:</span>
            <span class="value">{{ logicStats()!.flowCount }}</span>
          </div>
          <div class="stat">
            <span class="label">Compositions:</span>
            <span class="value">{{ logicStats()!.compositionCount }}</span>
          </div>
          <div class="stat">
            <span class="label">Blocks:</span>
            <span class="value">{{ logicStats()!.blockCount }}</span>
          </div>
        </div>
      }
      
      <div class="actions">
        <button (click)="openLogicBuilder()">
          Open Logic Builder
        </button>
        <button (click)="openFlowComposer()">
          Open Flow Composer
        </button>
      </div>
    </div>
  `
})
export class UniverseCardComponent implements OnInit {
  readonly universe = input.required<any>();
  readonly logicStats = signal<UniverseLogicMetadata | null>(null);
  
  private spaceMapIntegration = inject(SpaceMapIntegrationService);
  private router = inject(Router);
  
  async ngOnInit() {
    const stats = await this.spaceMapIntegration.getUniverseLogicStats(
      this.universe().id
    );
    this.logicStats.set(stats);
  }
  
  openLogicBuilder() {
    const url = this.spaceMapIntegration.getLogicBuilderUrl(this.universe().id);
    this.router.navigateByUrl(url);
  }
  
  openFlowComposer() {
    const url = this.spaceMapIntegration.getFlowComposerUrl(this.universe().id);
    this.router.navigateByUrl(url);
  }
}

Editor Integration

Purpose

The EditorIntegrationService allows editor components to:

Executing Flows from Widgets

import { EditorIntegrationService } from '@libs-structural/singularity-lib';

export class WidgetComponent {
  private editorIntegration = inject(EditorIntegrationService);
  
  async onButtonClick(event: MouseEvent) {
    // Execute bound flow
    const result = await this.editorIntegration.executeFlowFromWidget(
      'flow-id-123',
      { clickX: event.clientX, clickY: event.clientY },
      {
        widgetId: this.widgetId,
        pageId: this.pageId,
        universeId: this.universeId
      }
    );
    
    if (result.success) {
      console.log('Flow executed successfully:', result.data);
    } else {
      console.error('Flow execution failed:', result.error);
    }
  }
}

Getting Available Flows

For a flow selection dropdown:

export class PropertyPanelComponent {
  private editorIntegration = inject(EditorIntegrationService);
  
  readonly availableFlows = signal<FlowBinding[]>([]);
  
  async ngOnInit() {
    // Get flows for current universe
    const flows = await this.editorIntegration.getAvailableFlows(
      this.universeId
    );
    
    this.availableFlows.set(flows);
  }
}

Creating Widget Bindings

// Create a binding configuration
const binding = this.editorIntegration.createWidgetBinding({
  widgetId: 'submit-button',
  eventName: 'onClick',
  flowId: 'validation-flow',
  flowType: 'flow',
  dataMapping: {
    'formData': 'input',
    'userId': 'context.userId'
  },
  enabled: true
});

// Store binding in widget configuration
widget.bindings.onClick = binding;

Monitoring Execution Events

export class ExecutionMonitorComponent {
  private editorIntegration = inject(EditorIntegrationService);
  
  ngOnInit() {
    this.editorIntegration.executionEvents$.subscribe(event => {
      switch (event.type) {
        case 'start':
          console.log(`Flow ${event.flowId} started`);
          this.showLoadingIndicator();
          break;
          
        case 'complete':
          console.log(`Flow completed in ${event.executionTime}ms`);
          this.hideLoadingIndicator();
          break;
          
        case 'error':
          console.error(`Flow error: ${event.error}`);
          this.showErrorMessage(event.error);
          break;
      }
    });
  }
}

Complete Example: Interactive Button

import { Component, input, signal, inject } from '@angular/core';
import { EditorIntegrationService, FlowBinding } from '@libs-structural/singularity-lib';

@Component({
  selector: 'app-interactive-button',
  template: `
    <div class="interactive-button">
      <button 
        [disabled]="isExecuting()"
        (click)="handleClick($event)"
      >
        @if (isExecuting()) {
          <span class="spinner"></span>
        }
        {{ label() }}
      </button>
      
      @if (lastResult()) {
        <div class="result" [class.error]="!lastResult()!.success">
          {{ lastResult()!.success ? 'Success!' : 'Error: ' + lastResult()!.error }}
        </div>
      }
    </div>
  `
})
export class InteractiveButtonComponent {
  readonly label = input<string>('Click Me');
  readonly flowBinding = input<FlowBinding | null>(null);
  
  readonly isExecuting = signal(false);
  readonly lastResult = signal<any>(null);
  
  private editorIntegration = inject(EditorIntegrationService);
  
  async handleClick(event: MouseEvent) {
    const binding = this.flowBinding();
    if (!binding) return;
    
    this.isExecuting.set(true);
    this.lastResult.set(null);
    
    try {
      const result = await this.editorIntegration.executeFlowFromWidget(
        binding.flowId,
        { clickX: event.clientX, clickY: event.clientY },
        { 
          widgetId: binding.widgetId,
          // Additional context...
        }
      );
      
      this.lastResult.set(result);
    } finally {
      this.isExecuting.set(false);
    }
  }
}

Data Flow Examples

Example 1: User Creates Flow

User creates flow in Logic Builder
         ↓
Flow is saved with universeId
         ↓
SpaceMapIntegrationService.refreshUniverseStats(universeId)
         ↓
Space-map UI shows updated flow count
         ↓
User can now bind widgets to this flow

Example 2: Widget Triggers Flow

User clicks widget in Editor
         ↓
Editor checks widget.bindings.onClick
         ↓
EditorIntegrationService.executeFlowFromWidget()
         ↓
Flow executes in Singularity
         ↓
Result returned to Editor
         ↓
Widget updates based on result

Example 3: Universe Statistics Display

User opens universe detail in Space-Map
         ↓
SpaceMapIntegrationService.getUniverseLogicStats(universeId)
         ↓
Service queries Singularity database
         ↓
Returns { flowCount, compositionCount, blockCount }
         ↓
Space-map displays: "5 flows, 2 compositions"

Type Definitions

UniverseLogicMetadata

interface UniverseLogicMetadata {
  universeId: string;
  flowCount: number;
  compositionCount: number;
  blockCount: number;
  flowCategories: string[];
  sharedLogicBlocks: string[];
  sharedFlows: string[];
  lastAccessed: Date;
  modified: Date;
}

FlowBinding

interface FlowBinding {
  id: string;
  name: string;
  description: string;
  type: 'flow' | 'composition';
  inputs: FlowInput[];
  outputs: FlowOutput[];
}

FlowExecutionResult

interface FlowExecutionResult {
  success: boolean;
  data?: any;
  error?: string;
  executionTime: number;
  flowId: string;
}

WidgetBinding

interface WidgetBinding {
  id: string;
  widgetId: string;
  eventName: string;
  flowId: string;
  flowType: 'flow' | 'composition';
  dataMapping?: Record<string, string>;
  enabled: boolean;
  createdAt: Date;
}

Best Practices

For Space-Map Developers

  1. Always use integration service: Never access singularity services directly
  2. Cache appropriately: Use observable streams for reactive updates
  3. Handle errors: Statistics loading can fail, show fallback UI
  4. Refresh on changes: Call refreshUniverseStats after flow changes

For Editor Developers

  1. Validate bindings: Check flow exists before executing
  2. Handle errors gracefully: Flow execution can fail
  3. Show loading states: Execution can take time
  4. Monitor events: Use execution events for real-time feedback

For All Developers

  1. Type safety: Import types from @singularity-lib
  2. Error handling: Always handle promise rejections
  3. Performance: Cache data when appropriate
  4. Documentation: Document cross-library interactions

Troubleshooting

Problem: Statistics not updating

Solution: Call refreshUniverseStats after flow changes

await this.flowService.createFlow(data);
await this.spaceMapIntegration.refreshUniverseStats(universeId);

Problem: Flow execution fails

Solution: Check flow exists and is valid

const flows = await this.editorIntegration.getAvailableFlows(universeId);
const flowExists = flows.some(f => f.id === flowId);
if (!flowExists) {
  console.error('Flow not found');
}

Problem: Type errors

Solution: Import types correctly

import { 
  SpaceMapIntegrationService,
  EditorIntegrationService,
  UniverseLogicMetadata,
  FlowBinding
} from '@libs-structural/singularity-lib';

See Also


Remember: Integration services are the proper way to communicate between libraries. Never access services directly across library boundaries!