Skip to content

Activity Handler Types

Type utilities for cleaner activity implementations.

Overview

temporal-contract provides type utilities to extract activity handler types from your contracts, making activity implementations more maintainable and reusable.

Basic Usage

Instead of defining activity implementations inline, you can extract types for reuse:

typescript
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
import { orderContract } from './contract';

// Extract all activity handler types from contract
type OrderActivityHandlers = ActivityHandlers<typeof orderContract>;

// Implement activities with explicit types
const sendEmail: OrderActivityHandlers['sendEmail'] = async ({ to, body }) => {
  await emailService.send({ to, body });
  return { sent: true };
};

const processPayment: OrderActivityHandlers['processPayment'] = async ({ amount }) => {
  const txId = await paymentGateway.charge(amount);
  return { transactionId: txId };
};

// Use in handler
export const activities = declareActivitiesHandler({
  contract: orderContract,
  activities: {
    sendEmail,
    processPayment
  }
});

Type Utilities

ActivityHandlers

Extract all activity handler types from a contract:

typescript
import type { ActivityHandlers } from '@temporal-contract/worker/activity';

type MyActivities = ActivityHandlers<typeof myContract>;
// {
//   sendEmail: (input: { to: string, body: string }) => Promise<{ sent: boolean }>;
//   processPayment: (input: { amount: number }) => Promise<{ transactionId: string }>;
// }

Individual Activity Types

Extract specific activity types:

typescript
type SendEmailHandler = ActivityHandlers<typeof contract>['sendEmail'];
type ProcessPaymentHandler = ActivityHandlers<typeof contract>['processPayment'];

const sendEmail: SendEmailHandler = async ({ to, body }) => {
  // Implementation
  return { sent: true };
};

Benefits

1. Separation of Concerns

Implement activities in separate files:

typescript
// activities/email.ts
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
import { orderContract } from '../contracts/order.contract';

type Handlers = ActivityHandlers<typeof orderContract>;

export const sendEmail: Handlers['sendEmail'] = async ({ to, body }) => {
  await emailService.send({ to, body });
  return { sent: true };
};
typescript
// activities/payment.ts
import type { ActivityHandlers } from '@temporal-contract/worker/activity';
import { orderContract } from '../contracts/order.contract';

type Handlers = ActivityHandlers<typeof orderContract>;

export const processPayment: Handlers['processPayment'] = async ({ amount }) => {
  const txId = await paymentGateway.charge(amount);
  return { transactionId: txId };
};
typescript
// activities/index.ts
import { declareActivitiesHandler } from '@temporal-contract/worker/activity';
import { orderContract } from '../contracts/order.contract';
import { sendEmail } from './email';
import { processPayment } from './payment';

export const activities = declareActivitiesHandler({
  contract: orderContract,
  activities: {
    sendEmail,
    processPayment
  }
});

2. Dependency Injection

Create factory functions with typed activities:

typescript
import type { ActivityHandlers } from '@temporal-contract/worker/activity';

type Handlers = ActivityHandlers<typeof orderContract>;

export const createEmailActivity = (
  emailService: EmailService
): Handlers['sendEmail'] => {
  return async ({ to, body }) => {
    await emailService.send({ to, body });
    return { sent: true };
  };
};

export const createPaymentActivity = (
  paymentGateway: PaymentGateway
): Handlers['processPayment'] => {
  return async ({ amount }) => {
    const txId = await paymentGateway.charge(amount);
    return { transactionId: txId };
  };
};

Usage:

typescript
const emailService = new EmailService();
const paymentGateway = new PaymentGateway();

export const activities = declareActivitiesHandler({
  contract: orderContract,
  activities: {
    sendEmail: createEmailActivity(emailService),
    processPayment: createPaymentActivity(paymentGateway)
  }
});

3. Testability

Mock activities with correct types:

typescript
import type { ActivityHandlers } from '@temporal-contract/worker/activity';

type Handlers = ActivityHandlers<typeof orderContract>;

// Create mock activities for testing
const mockActivities: Handlers = {
  sendEmail: async ({ to, body }) => ({ sent: true }),
  processPayment: async ({ amount }) => ({ transactionId: 'TEST-TXN' })
};

// Use in tests
describe('processOrder', () => {
  it('should process payment', async () => {
    const context = createMockContext(mockActivities);
    const result = await processOrder.implementation(context, {
      orderId: 'ORD-123'
    });
    expect(result.success).toBe(true);
  });
});

Advanced Patterns

Middleware Pattern

Wrap activities with middleware:

typescript
import type { ActivityHandlers } from '@temporal-contract/worker/activity';

type Handlers = ActivityHandlers<typeof orderContract>;

// Create logging middleware
function withLogging<T extends (...args: any[]) => any>(
  name: string,
  fn: T
): T {
  return (async (...args: any[]) => {
    console.log(`[${name}] Starting`, args);
    try {
      const result = await fn(...args);
      console.log(`[${name}] Success`, result);
      return result;
    } catch (error) {
      console.error(`[${name}] Error`, error);
      throw error;
    }
  }) as T;
}

// Apply to activities
const sendEmail: Handlers['sendEmail'] = withLogging(
  'sendEmail',
  async ({ to, body }) => {
    await emailService.send({ to, body });
    return { sent: true };
  }
);

Retry Logic

Add retry logic to activities:

typescript
function withRetry<T extends (...args: any[]) => Promise<any>>(
  fn: T,
  maxRetries = 3
): T {
  return (async (...args: any[]) => {
    let lastError;
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await fn(...args);
      } catch (error) {
        lastError = error;
        if (i < maxRetries - 1) {
          await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
        }
      }
    }
    throw lastError;
  }) as T;
}

const processPayment: Handlers['processPayment'] = withRetry(
  async ({ amount }) => {
    const txId = await paymentGateway.charge(amount);
    return { transactionId: txId };
  },
  3
);

Caching Pattern

Add caching to expensive activities:

typescript
const cache = new Map<string, any>();

function withCache<T extends (input: any) => Promise<any>>(
  fn: T,
  keyFn: (input: any) => string
): T {
  return (async (input: any) => {
    const key = keyFn(input);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = await fn(input);
    cache.set(key, result);
    return result;
  }) as T;
}

const validateInventory: Handlers['validateInventory'] = withCache(
  async ({ orderId }) => {
    const available = await inventoryDB.check(orderId);
    return { available };
  },
  ({ orderId }) => orderId
);

Best Practices

1. Use Type Utilities

Always extract types for better maintainability:

typescript
// ✅ Good
type Handlers = ActivityHandlers<typeof contract>;
const sendEmail: Handlers['sendEmail'] = async ({ to, body }) => { /* ... */ };

// ❌ Avoid inline typing
const sendEmail = async ({ to, body }: { to: string, body: string }) => { /* ... */ };

2. Organize by Domain

Group related activities:

typescript
// activities/payment/index.ts
export const processPayment: Handlers['processPayment'] = /* ... */;
export const refundPayment: Handlers['refundPayment'] = /* ... */;

// activities/email/index.ts
export const sendEmail: Handlers['sendEmail'] = /* ... */;
export const sendBulkEmail: Handlers['sendBulkEmail'] = /* ... */;

3. Use Dependency Injection

Make activities testable and configurable:

typescript
export const createActivities = (services: Services) => {
  const sendEmail: Handlers['sendEmail'] = async ({ to, body }) => {
    await services.email.send({ to, body });
    return { sent: true };
  };

  return { sendEmail };
};

See Also

Released under the MIT License.