Skip to main content

Overview

The BaseMember class is the abstract base class that all member types extend. It provides common functionality for execution, validation, caching, retry logic, and lifecycle hooks.
import { BaseMember } from '@ensemble-edge/conductor';

class CustomMember extends BaseMember {
  async execute(input: any): Promise<any> {
    // Implementation
  }
}

Abstract Class

abstract class BaseMember {
  abstract execute(input: any): Promise<any>;
}
All custom members must extend BaseMember and implement the execute() method.

Constructor

constructor(options: MemberOptions)
options
MemberOptions
required
Member configuration options
options.name
string
required
Unique member name
options.type
string
required
Member type: Think, Function, Data, API
options.config
object
Type-specific configuration
options.cache
CacheConfig
Caching configuration
options.retry
RetryConfig
Retry configuration
options.timeout
number
default:"30000"
Execution timeout in milliseconds
options.metadata
object
Additional metadata
interface MemberOptions {
  name: string;
  type: string;
  config?: Record<string, any>;
  cache?: CacheConfig;
  retry?: RetryConfig;
  timeout?: number;
  metadata?: Record<string, any>;
}

Abstract Methods

execute()

Execute the member logic (must be implemented).
abstract execute(input: any): Promise<any>
input
any
required
Input data for execution
Returns: Promise<any> - Execution result Example Implementation:
class ValidateEmailMember extends BaseMember {
  async execute(input: { email: string }): Promise<{ valid: boolean }> {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return {
      valid: emailRegex.test(input.email)
    };
  }
}

Lifecycle Hooks

beforeExecute()

Called before execute(), useful for validation and setup.
async beforeExecute(input: any): Promise<void>
Example:
class CustomMember extends BaseMember {
  async beforeExecute(input: any): Promise<void> {
    // Validate input
    if (!input.userId) {
      throw new Error('userId is required');
    }

    // Log execution start
    console.log(`Executing ${this.name} for user ${input.userId}`);

    // Setup resources
    await this.initializeResources();
  }

  async execute(input: any): Promise<any> {
    // Main logic
  }
}

afterExecute()

Called after execute(), useful for cleanup and post-processing.
async afterExecute(output: any): Promise<any>
output
any
required
Output from execute()
Returns: Promise<any> - Transformed output Example:
class CustomMember extends BaseMember {
  async execute(input: any): Promise<any> {
    return { result: 'success', data: input };
  }

  async afterExecute(output: any): Promise<any> {
    // Transform output
    output.timestamp = Date.now();
    output.member = this.name;

    // Log result
    console.log(`${this.name} completed:`, output);

    // Cleanup
    await this.cleanup();

    return output;
  }
}

onError()

Called when execute() throws an error.
async onError(error: Error, input: any): Promise<void>
error
Error
required
Error that occurred
input
any
required
Input that caused error
Example:
class CustomMember extends BaseMember {
  async onError(error: Error, input: any): Promise<void> {
    // Log error with context
    console.error(`Error in ${this.name}:`, {
      error: error.message,
      input,
      timestamp: Date.now()
    });

    // Send alert
    await this.sendErrorAlert(error, input);

    // Cleanup resources
    await this.cleanup();

    // Re-throw or transform error
    throw new MemberError(
      `${this.name} failed: ${error.message}`,
      { cause: error, input }
    );
  }

  async execute(input: any): Promise<any> {
    // Implementation
  }
}

Properties

name

readonly name: string
Unique member name.

type

readonly type: string
Member type: Think, Function, Data, API.

config

readonly config: Record<string, any>
Type-specific configuration.

timeout

readonly timeout: number
Execution timeout in milliseconds.

metadata

readonly metadata: Record<string, any>
Additional metadata.

Caching

Cache Configuration

interface CacheConfig {
  enabled: boolean;
  ttl: number;
  key?: string | ((input: any) => string);
  namespace?: string;
  invalidateOn?: string[];
}
Example:
- member: fetch-user
  type: Function
  cache:
    enabled: true
    ttl: 300000  # 5 minutes
    key: ${input.userId}
    namespace: users

getCacheKey()

Generate cache key for input.
getCacheKey(input: any): string
Override for custom keys:
class CustomMember extends BaseMember {
  getCacheKey(input: any): string {
    // Custom cache key logic
    return `${this.name}:${input.userId}:${input.date}`;
  }

  async execute(input: any): Promise<any> {
    // Implementation
  }
}

shouldCache()

Determine if result should be cached.
shouldCache(input: any, output: any): boolean
Override for conditional caching:
class CustomMember extends BaseMember {
  shouldCache(input: any, output: any): boolean {
    // Only cache successful results
    return output.status === 'success';
  }

  async execute(input: any): Promise<any> {
    // Implementation
  }
}

Retry Logic

Retry Configuration

interface RetryConfig {
  maxAttempts: number;
  backoff: 'fixed' | 'exponential' | 'linear';
  initialDelay?: number;
  maxDelay?: number;
  retryIf?: (error: Error) => boolean;
}
Example:
- member: api-call
  type: API
  retry:
    maxAttempts: 3
    backoff: exponential
    initialDelay: 1000
    maxDelay: 10000

shouldRetry()

Determine if execution should be retried.
shouldRetry(error: Error, attempt: number): boolean
Override for custom retry logic:
class CustomMember extends BaseMember {
  shouldRetry(error: Error, attempt: number): boolean {
    // Retry on network errors
    if (error.name === 'NetworkError') {
      return attempt < 3;
    }

    // Don't retry validation errors
    if (error instanceof ValidationError) {
      return false;
    }

    // Default retry logic
    return super.shouldRetry(error, attempt);
  }

  async execute(input: any): Promise<any> {
    // Implementation
  }
}

Validation

validateInput()

Validate input before execution.
validateInput(input: any): void
Example:
class CustomMember extends BaseMember {
  validateInput(input: any): void {
    if (!input.email) {
      throw new ValidationError('email is required');
    }

    if (!input.email.includes('@')) {
      throw new ValidationError('invalid email format');
    }
  }

  async execute(input: any): Promise<any> {
    this.validateInput(input);
    // Implementation
  }
}

validateOutput()

Validate output after execution.
validateOutput(output: any): void
Example:
class CustomMember extends BaseMember {
  async execute(input: any): Promise<any> {
    const output = { /* ... */ };
    this.validateOutput(output);
    return output;
  }

  validateOutput(output: any): void {
    if (!output.userId) {
      throw new ValidationError('userId missing in output');
    }

    if (typeof output.userId !== 'string') {
      throw new ValidationError('userId must be a string');
    }
  }
}

Context Access

getContext()

Access execution context.
getContext(): ExecutionContext
interface ExecutionContext {
  executionId: string;
  parentExecutionId?: string;
  input: Record<string, any>;
  memberOutputs: Map<string, any>;
  env: Env;
  metadata: Record<string, any>;
}
Example:
class CustomMember extends BaseMember {
  async execute(input: any): Promise<any> {
    const context = this.getContext();

    console.log('Execution ID:', context.executionId);
    console.log('Original input:', context.input);

    // Access previous member outputs
    const previousResult = context.memberOutputs.get('previous-member');

    // Access environment
    const apiKey = context.env.API_KEY;

    return { /* ... */ };
  }
}

Creating Custom Members

Basic Custom Member

import { BaseMember } from '@ensemble-edge/conductor';

class TransformDataMember extends BaseMember {
  async execute(input: { data: any[] }): Promise<{ transformed: any[] }> {
    const transformed = input.data.map(item => ({
      ...item,
      processed: true,
      timestamp: Date.now()
    }));

    return { transformed };
  }
}

With Lifecycle Hooks

class EnhancedMember extends BaseMember {
  async beforeExecute(input: any): Promise<void> {
    console.log(`Starting ${this.name}`);
    this.validateInput(input);
  }

  async execute(input: any): Promise<any> {
    // Main logic
    return { result: 'success' };
  }

  async afterExecute(output: any): Promise<any> {
    output.executedAt = Date.now();
    output.member = this.name;
    return output;
  }

  async onError(error: Error, input: any): Promise<void> {
    console.error(`Error in ${this.name}:`, error);
    // Send notification
    await this.notifyError(error, input);
  }

  validateInput(input: any): void {
    if (!input.required) {
      throw new Error('Missing required field');
    }
  }
}

With Caching

class CachedAPIMember extends BaseMember {
  getCacheKey(input: any): string {
    return `api:${input.endpoint}:${JSON.stringify(input.params)}`;
  }

  shouldCache(input: any, output: any): boolean {
    return output.status === 200;
  }

  async execute(input: { endpoint: string; params: object }): Promise<any> {
    const response = await fetch(input.endpoint, {
      method: 'POST',
      body: JSON.stringify(input.params)
    });

    return {
      status: response.status,
      data: await response.json()
    };
  }
}

Testing

import { BaseMember } from '@ensemble-edge/conductor';
import { describe, it, expect, beforeEach } from 'vitest';

class TestMember extends BaseMember {
  async execute(input: { value: number }): Promise<{ doubled: number }> {
    return { doubled: input.value * 2 };
  }
}

describe('TestMember', () => {
  let member: TestMember;

  beforeEach(() => {
    member = new TestMember({
      name: 'test-member',
      type: 'Function'
    });
  });

  it('executes successfully', async () => {
    const result = await member.execute({ value: 5 });
    expect(result.doubled).toBe(10);
  });

  it('handles errors', async () => {
    const errorMember = new TestMember({
      name: 'error-member',
      type: 'Function'
    });

    errorMember.execute = async () => {
      throw new Error('Test error');
    };

    await expect(errorMember.execute({})).rejects.toThrow('Test error');
  });

  it('calls lifecycle hooks', async () => {
    const calls: string[] = [];

    member.beforeExecute = async () => {
      calls.push('before');
    };

    member.afterExecute = async (output) => {
      calls.push('after');
      return output;
    };

    await member.execute({ value: 5 });

    expect(calls).toEqual(['before', 'after']);
  });
});

Best Practices

  1. Implement all lifecycle hooks - For robust error handling
  2. Validate input - Fail fast on bad data
  3. Use descriptive names - Clear member identification
  4. Handle errors gracefully - Provide context
  5. Cache when appropriate - Improve performance
  6. Set reasonable timeouts - Prevent hanging
  7. Log important events - Aid debugging
  8. Test thoroughly - All code paths
  9. Document behavior - Clear expectations
  10. Keep execute() focused - Single responsibility