Inter-Module Communication Patterns

Complete guide to EventBus and ActionRegistry patterns for module communication

Inter-Module Communication Patterns

This document provides the complete reference for how modules communicate with each other in GoGrab Space.


Two Communication Mechanisms

1. EventBus (Reactive Events)

Use for: Broadcasting state changes, notifications, lifecycle events

2. ActionRegistry (Command Pattern)

Use for: Invoking operations, requesting data, triggering workflows


EventBus Pattern

Emitting Events

import { inject } from '@angular/core';
import { EventBusService } from '@libs-core/event-system';

export class UserManagementModule {
  private readonly eventBus = inject(EventBusService);

  async createUser(userData: CreateUserInput) {
    const user = await this.userService.create(userData);
    
    // Emit event for other modules to react
    this.eventBus.emit('user:created', {
      userId: user.id,
      email: user.email,
      tenantId: context.tenantId,
      timestamp: Date.now()
    });
    
    return user;
  }
}

Listening to Events

import { inject, OnInit } from '@angular/core';
import { EventBusService } from '@libs-core/event-system';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class AnalyticsModule implements OnInit {
  private readonly eventBus = inject(EventBusService);
  private readonly destroyRef = inject(DestroyRef);

  ngOnInit() {
    // Listen for user creation events
    this.eventBus.on('user:created').pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(event => {
      this.trackUserCreation({
        userId: event.userId,
        source: 'user-management',
        timestamp: event.timestamp
      });
    });
  }
}

Standard Event Names

Event Payload Description
user:created { userId, email, tenantId } New user created
user:updated { userId, changes, tenantId } User data updated
user:deleted { userId, tenantId } User deleted
module:loaded { moduleId, version } Module initialized
module:error { moduleId, error } Module error occurred
data:changed { collection, documentId } Data mutation

ActionRegistry Pattern

Defining Public Actions

import { Injectable, inject } from '@angular/core';
import { Observable, of, map, catchError } from 'rxjs';
import {
  PublicAction,
  ExecutableAction,
  ActionContext,
  ActionParams,
  ActionParameterBuilder,
  ParameterBuilders,
} from '@libs-core/action-registry';
import { EventBusService } from '@libs-core/event-system';
import { LoggerService } from '@libs-core/log-system';

export interface CreateUserInput extends ActionParams {
  email: string;
  firstName: string;
  lastName: string;
  roleId: string;
  tenantId: string;
}

export interface CreateUserOutput {
  success: boolean;
  userId?: string;
  error?: string;
}

@PublicAction({
  id: 'users:create-user',
  name: 'Create User',
  description: 'Create a new user with role assignment',
  permissions: ['users:create'],
  tags: ['users', 'crud', 'creation'],
  parameters: [
    ParameterBuilders.email('email')
      .label('Email Address')
      .helpText('User email address for login')
      .build(),
    ActionParameterBuilder.string('firstName')
      .description('User first name')
      .required('First name is required')
      .minLength(2, 'First name must be at least 2 characters')
      .maxLength(50, 'First name must not exceed 50 characters')
      .label('First Name')
      .placeholder('John')
      .build(),
    ActionParameterBuilder.string('lastName')
      .description('User last name')
      .required('Last name is required')
      .label('Last Name')
      .build(),
    ActionParameterBuilder.string('roleId')
      .description('Role ID to assign')
      .required('Role is required')
      .label('Role')
      .build(),
  ],
})
@Injectable({ providedIn: 'root' })
export class CreateUserAction implements ExecutableAction<CreateUserInput, CreateUserOutput> {
  private readonly userService = inject(UserService);
  private readonly eventBus = inject(EventBusService);
  private readonly logger = inject(LoggerService).child({ source: 'CreateUserAction' });

  execute(params: CreateUserInput, context: ActionContext): Observable<CreateUserOutput> {
    this.logger.info('Executing CreateUser action', { email: params.email });

    return this.userService.createUser({
      email: params.email,
      firstName: params.firstName,
      lastName: params.lastName,
      roleId: params.roleId,
    }).pipe(
      map(user => {
        // Emit event for other modules
        this.eventBus.emit('user:created', {
          userId: user.id,
          tenantId: params.tenantId,
          timestamp: Date.now()
        });

        return { success: true, userId: user.id };
      }),
      catchError(error => {
        this.logger.error('Failed to create user', { error });
        return of({ success: false, error: error.message });
      })
    );
  }
}

Invoking Actions from Other Modules

import { inject } from '@angular/core';
import { ActionRegistryService } from '@libs-core/action-registry';

export class OnboardingModule {
  private readonly actionRegistry = inject(ActionRegistryService);

  async onboardNewUser(email: string, name: string) {
    // Invoke user-management module's action
    const result = await firstValueFrom(
      this.actionRegistry.execute('users:create-user', {
        email,
        firstName: name.split(' ')[0],
        lastName: name.split(' ')[1] || '',
        roleId: 'default-user-role',
        tenantId: this.contextService.currentTenantId()
      })
    );

    if (result.success) {
      console.log('User created:', result.userId);
      // Continue onboarding...
    } else {
      console.error('Failed to create user:', result.error);
    }
  }
}

Registering Actions in Module Manifest

export class UserManagementModule implements BrowserModuleContract {
  readonly manifest: ModuleManifest = {
    id: 'user-management',
    version: '1.0.0',
    name: 'User Management',
    
    // Public actions available to other modules
    publicActions: [
      CreateUserAction,
      UpdateUserAction,
      DeleteUserAction,
      GetUserAction,
      ListUsersAction,
    ],
  };
}

Widget Flow Execution

Widgets can trigger module actions via the flow executor:

import { Component, inject } from '@angular/core';
import { WIDGET_FLOW_EXECUTOR_SERVICE } from '@libs-foundational/core-angular';
import type { IWidgetFlowExecutorService } from '@libs-foundational/core-models';

@Component({
  selector: 'lib-button-widget',
  template: `
    <ui-button
      [text]="buttonText()"
      (click)="handleClick($event)"
    />
  `
})
export class ButtonWidgetComponent {
  // Optional injection - won't crash if Singularity not loaded
  private readonly flowExecutor = inject<IWidgetFlowExecutorService>(
    WIDGET_FLOW_EXECUTOR_SERVICE,
    { optional: true }
  );

  async handleClick(event: MouseEvent) {
    const config = this.config();
    
    if (config.action === 'flow' && config.flowId && this.flowExecutor) {
      // Execute Singularity flow
      await this.flowExecutor.executeFlow(config.flowId, {
        widgetId: this.widget().id,
        event: 'click',
        mouseX: event.clientX,
        mouseY: event.clientY
      });
    } else if (config.action === 'module-action' && config.actionId) {
      // Execute module action directly
      const actionRegistry = inject(ActionRegistryService);
      actionRegistry.execute(config.actionId, config.actionParams).subscribe();
    }
  }
}

Best Practices

1. Event Naming Convention

2. Action Naming Convention

3. Type Safety

4. Error Handling

5. Permissions

6. Memory Management


Complete Example: Two Modules Communicating

Module A: User Management (Provider)

// actions/create-user.action.ts
@PublicAction({
  id: 'users:create-user',
  name: 'Create User',
  permissions: ['users:create'],
  parameters: [/* ... */],
})
@Injectable({ providedIn: 'root' })
export class CreateUserAction implements ExecutableAction {
  private readonly eventBus = inject(EventBusService);

  execute(params, context): Observable<CreateUserOutput> {
    return this.userService.create(params).pipe(
      tap(user => {
        // Emit event
        this.eventBus.emit('user:created', {
          userId: user.id,
          email: user.email
        });
      }),
      map(user => ({ success: true, userId: user.id }))
    );
  }
}

// user-management.module.ts
export class UserManagementModule implements BrowserModuleContract {
  readonly manifest: ModuleManifest = {
    id: 'user-management',
    publicActions: [CreateUserAction],
  };
}

Module B: Analytics (Consumer)

// analytics.module.ts
export class AnalyticsModule implements BrowserModuleContract {
  private readonly eventBus = inject(EventBusService);
  private readonly actionRegistry = inject(ActionRegistryService);

  initialize(): Observable<void> {
    // Listen to user creation events
    this.eventBus.on('user:created').subscribe(event => {
      this.trackEvent('user_created', {
        userId: event.userId,
        timestamp: Date.now()
      });
    });

    return of(void 0);
  }

  // Can also invoke user actions
  async getUserData(userId: string) {
    const result = await firstValueFrom(
      this.actionRegistry.execute('users:get-user', { userId })
    );
    return result.data;
  }
}

Testing Inter-Module Communication

// test/inter-module-communication.spec.ts
describe('Inter-Module Communication', () => {
  let eventBus: EventBusService;
  let actionRegistry: ActionRegistryService;

  beforeEach(() => {
    eventBus = TestBed.inject(EventBusService);
    actionRegistry = TestBed.inject(ActionRegistryService);
  });

  it('should emit event when user created', (done) => {
    eventBus.on('user:created').subscribe(event => {
      expect(event.userId).toBeDefined();
      expect(event.email).toBe('[email protected]');
      done();
    });

    // Trigger user creation
    actionRegistry.execute('users:create-user', {
      email: '[email protected]',
      firstName: 'Test',
      lastName: 'User',
      roleId: 'user',
      tenantId: 'test-tenant'
    }).subscribe();
  });

  it('should invoke action from another module', async () => {
    const result = await firstValueFrom(
      actionRegistry.execute('users:create-user', {
        email: '[email protected]',
        firstName: 'Test',
        lastName: 'User',
        roleId: 'user',
        tenantId: 'test-tenant'
      })
    );

    expect(result.success).toBe(true);
    expect(result.userId).toBeDefined();
  });
});

Last Updated: December 9, 2025 - Session 10.2