documentation/topics/lifecycle-hooks.md

<!--
SPDX-FileCopyrightText: 2025 Torkild G. Kjevik
SPDX-FileCopyrightText: 2025 ash_typescript contributors <https://github.com/ash-project/ash_typescript/graphs.contributors>

SPDX-License-Identifier: MIT
-->

# Lifecycle Hooks

AshTypescript provides comprehensive lifecycle hooks for both HTTP and Phoenix Channel-based RPC actions. These hooks enable cross-cutting concerns like logging, telemetry, performance tracking, and error monitoring. HTTP hooks additionally support authentication header injection.

## Table of Contents

- [HTTP Lifecycle Hooks](#http-lifecycle-hooks)
  - [Why Use HTTP Lifecycle Hooks?](#why-use-http-lifecycle-hooks)
  - [HTTP Configuration](#http-configuration)
  - [Hook Types: Actions vs Validations](#hook-types-actions-vs-validations)
  - [Hook Function Signatures](#hook-function-signatures)
  - [beforeRequest Hook](#beforerequest-hook)
  - [afterRequest Hook](#afterrequest-hook)
  - [Config Precedence Rules](#config-precedence-rules)
  - [Exception Handling](#exception-handling)
  - [Complete Working Example](#complete-working-example)
- [Channel Lifecycle Hooks](#channel-lifecycle-hooks)
  - [Why Use Channel Lifecycle Hooks?](#why-use-channel-lifecycle-hooks)
  - [Key Differences from HTTP Hooks](#key-differences-from-http-hooks)
  - [Channel Configuration](#channel-configuration)
  - [Channel Hook Function Signatures](#channel-hook-function-signatures)
  - [beforeChannelPush Hook](#beforechannelpush-hook)
  - [afterChannelResponse Hook](#afterchannelresponse-hook)
  - [Channel Config Precedence Rules](#channel-config-precedence-rules)
  - [Complete Channel Working Example](#complete-channel-working-example)
- [Troubleshooting](#troubleshooting)

## HTTP Lifecycle Hooks

AshTypescript provides lifecycle hooks that let you inject custom logic before and after HTTP requests. These hooks enable cross-cutting concerns like authentication, logging, telemetry, performance tracking, and error monitoring.

### Why Use HTTP Lifecycle Hooks?

Lifecycle hooks provide a centralized way to:
- **Add authentication tokens** - Automatically inject auth headers from localStorage
- **Log requests and responses** - Track API calls for debugging
- **Measure performance** - Time API calls and track latency
- **Send telemetry** - Report metrics to monitoring services
- **Handle errors globally** - Track errors in Sentry, Datadog, etc.
- **Add correlation IDs** - Track requests across distributed systems
- **Add default headers** - Set client version, request IDs, etc.
- **Transform requests** - Modify config before sending

### HTTP Configuration

Configure lifecycle hooks in your application config:

```elixir
# config/config.exs
config :ash_typescript,
  # Hook functions for RPC actions
  rpc_action_before_request_hook: "RpcHooks.beforeRequest",
  rpc_action_after_request_hook: "RpcHooks.afterRequest",

  # Hook functions for validation actions
  rpc_validation_before_request_hook: "RpcHooks.beforeValidationRequest",
  rpc_validation_after_request_hook: "RpcHooks.afterValidationRequest",

  # TypeScript types for hook context (optional)
  rpc_action_hook_context_type: "RpcHooks.ActionHookContext",
  rpc_validation_hook_context_type: "RpcHooks.ValidationHookContext",

  # Import the module containing your hook functions
  import_into_generated: [
    %{
      import_name: "RpcHooks",
      file: "./rpcHooks"
    }
  ]
```

**Configuration Options:**

| Config | Purpose | Default |
|--------|---------|---------|
| `rpc_action_before_request_hook` | Function called before RPC action requests | `nil` (disabled) |
| `rpc_action_after_request_hook` | Function called after RPC action requests | `nil` (disabled) |
| `rpc_validation_before_request_hook` | Function called before validation requests | `nil` (disabled) |
| `rpc_validation_after_request_hook` | Function called after validation requests | `nil` (disabled) |
| `rpc_action_hook_context_type` | TypeScript type for action hook context | `"Record<string, any>"` |
| `rpc_validation_hook_context_type` | TypeScript type for validation hook context | `"Record<string, any>"` |

### Hook Types: Actions vs Validations

AshTypescript provides **separate hooks for actions and validations** because they serve different purposes:

- **Action Hooks** - Execute when calling RPC actions (create, read, update, delete, custom actions)
- **Validation Hooks** - Execute when calling validation functions (client-side form validation)

This separation allows you to:
- Use different logging levels (validations are typically more frequent)
- Track different metrics (validation performance vs action performance)

**Action hooks are for actual API calls, validation hooks are for form validation.**

### Hook Function Signatures

Both `beforeRequest` and `afterRequest` hooks receive the full config object and can access the optional `hookCtx` from it.

**Important:** AshTypescript exports `ActionConfig` and `ValidationConfig` types from the generated file. These types automatically include your custom `hookCtx` types based on your configuration settings.

#### Configuring Custom Hook Context Types

When you configure context type settings in your Elixir config, the generated TypeScript interfaces will automatically include these types:

```elixir
# config/config.exs
config :ash_typescript,
  # TypeScript types for hook context
  rpc_action_hook_context_type: "RpcHooks.ActionHookContext",
  rpc_validation_hook_context_type: "RpcHooks.ValidationHookContext"
```

With this configuration, the generated `ActionConfig` and `ValidationConfig` types will have properly typed `hookCtx` fields:

```typescript
// Generated types (in your generated file)
export interface ActionConfig {
  // ... other fields ...
  hookCtx?: RpcHooks.ActionHookContext;  // ← Your custom type
}

export interface ValidationConfig {
  // ... other fields ...
  hookCtx?: RpcHooks.ValidationHookContext;  // ← Your custom type
}
```

#### Implementing Hook Functions

Simply import and use the generated config types directly - no generics needed!

```typescript
// rpcHooks.ts - Define your custom hook context interfaces
export interface ActionHookContext {
  enableLogging?: boolean;
  enableTiming?: boolean;
  customHeaders?: Record<string, string>;
  startTime?: number;
}

export interface ValidationHookContext {
  enableLogging?: boolean;
  validationLevel?: "strict" | "normal";
}

// Import the generated config types
import type { ActionConfig, ValidationConfig } from './generated';

// Implement your hook functions - the hookCtx is already properly typed!
export async function beforeActionRequest(
  actionName: string,
  config: ActionConfig
): Promise<ActionConfig> {
  const ctx = config.hookCtx;

  // ctx is automatically typed as ActionHookContext | undefined
  if (ctx?.enableLogging) {
    console.log(`[Action] ${actionName} started`);
  }

  // Modify hookCtx if needed
  const modifiedCtx = ctx ? { ...ctx, startTime: Date.now() } : undefined;

  return {
    ...config,
    ...(modifiedCtx && { hookCtx: modifiedCtx })
  };
}

export async function afterActionRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ActionConfig
): Promise<void> {
  const ctx = config.hookCtx;

  // ctx.startTime is properly typed (no type assertion needed!)
  if (ctx?.enableTiming && ctx.startTime) {
    const duration = Date.now() - ctx.startTime;
    console.log(`Request took ${duration}ms`);
  }
}

// Similarly for validation hooks
export async function beforeValidationRequest(
  actionName: string,
  config: ValidationConfig
): Promise<ValidationConfig> {
  const ctx = config.hookCtx;

  if (ctx?.validationLevel === "strict") {
    console.log(`[Validation] Running in strict mode`);
  }

  return config;
}
```

**Key Benefits:**
- **Type safety** - Your custom context fields are properly typed automatically
- **IntelliSense** - IDE autocomplete works for your custom fields
- **No generics needed** - The generated types already include your context types
- **Simpler code** - Direct usage without complex generic constraints

The exported `ActionConfig` interface includes all available configuration fields:

```typescript
// This type is exported from your generated file
export interface ActionConfig {
  // Request data
  input?: Record<string, any>;
  primaryKey?: any;
  fields?: Array<string | Record<string, any>>; // Field selection
  filter?: Record<string, any>; // Filter options (for reads)
  sort?: string; // Sort options
  page?:
    | {
        // Offset-based pagination
        limit?: number;
        offset?: number;
        count?: boolean;
      }
    | {
        // Keyset pagination
        limit?: number;
        after?: string;
        before?: string;
      };

  // Metadata
  metadataFields?: Record<string, any>; // Metadata field selection

  // HTTP customization
  headers?: Record<string, string>; // Custom headers
  fetchOptions?: RequestInit; // Fetch options (signal, cache, etc.)
  customFetch?: (
    input: RequestInfo | URL,
    init?: RequestInit,
  ) => Promise<Response>;

  // Multitenancy
  tenant?: string; // Tenant parameter

  // Hook context
  hookCtx?: Record<string, any>;
}

// This type is also exported from your generated file
export interface ValidationConfig {
  // Request data
  input?: Record<string, any>;

  // HTTP customization
  headers?: Record<string, string>;
  fetchOptions?: RequestInit;
  customFetch?: (
    input: RequestInfo | URL,
    init?: RequestInit,
  ) => Promise<Response>;

  // Hook context
  hookCtx?: Record<string, any>;
}
```

**Key Points:**
- Hooks receive the entire `config` object as a parameter
- Hook context is accessed via `config.hookCtx` (optional)
- `beforeRequest` returns a modified config object
- `afterRequest` returns nothing (void) - it's for side effects only
- Hooks run unconditionally when configured (not gated by `hookCtx` presence)

### beforeRequest Hook

The `beforeRequest` hook runs **before the HTTP request** and can modify the request configuration. Common use cases:

#### Adding Authentication Tokens

```typescript
// rpcHooks.ts
import type { ActionConfig } from './generated';

export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  // Fetch auth token from localStorage (if it exists)
  const authToken = localStorage.getItem('authToken');

  // Add authentication header if token is present
  if (authToken) {
    return {
      ...config,
      headers: {
        ...config.headers,
        'Authorization': `Bearer ${authToken}`
      }
    };
  }

  return config;
}
```

This pattern automatically adds authentication to all RPC requests without needing to pass tokens through every call. The hook centralizes auth header logic in one place.

```typescript
// Usage: Auth headers are added automatically
const todos = await listTodos({
  fields: ["id", "title"]
  // No need to pass auth tokens - hook handles it!
});
```

#### Adding Correlation IDs for Request Tracking

```typescript
// rpcHooks.ts
import type { ActionConfig } from './generated';

export interface ActionHookContext {
  correlationId?: string;
}

export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  const ctx = config.hookCtx;

  // Use provided correlation ID or generate one
  const correlationId = ctx?.correlationId || generateRequestId();

  return {
    ...config,
    headers: {
      'X-Client-Version': '1.0.0',
      'X-Correlation-ID': correlationId,
      'X-Request-ID': correlationId,
      ...config.headers  // Original headers take precedence
    }
  };
}

function generateRequestId(): string {
  return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
```

```typescript
// Usage: Pass correlation ID for distributed tracing
const todos = await listTodos({
  fields: ["id", "title"],
  hookCtx: {
    correlationId: 'user-dashboard-load-456'
  }
});
```

#### Request Timing Setup

```typescript
export interface ActionHookContext {
  startTime?: number;
}

export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  const ctx = config.hookCtx;

  // Store request start time in context for afterRequest hook
  if (ctx) {
    ctx.startTime = Date.now();
  }

  return config;
}
```

#### Logging Outgoing Requests

```typescript
export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  const ctx = config.hookCtx;

  console.log('Outgoing RPC request:', {
    action: actionName,
    domain: config.domain,
    hasInput: !!config.input,
    timestamp: new Date().toISOString(),
    correlationId: ctx?.correlationId
  });

  return config;
}
```

### afterRequest Hook

The `afterRequest` hook runs **after the HTTP request completes** (both success and error) and is used for side effects. It receives three parameters:

1. `response: Response` - The raw HTTP response object
2. `result: any | null` - Parsed JSON result (null when `response.ok` is false)
3. `config: ActionConfig` - The config used for the request

#### Important: Null Result Handling

The `afterRequest` hook receives `null` as the result parameter when the response is not OK:

```typescript
export function afterRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ActionConfig
): void {
  if (result === null) {
    // Response failed (response.ok === false)
    console.error('Request failed:', {
      status: response.status,
      statusText: response.statusText,
      url: response.url
    });
  } else {
    // Response succeeded (response.ok === true)
    console.log('Request succeeded:', {
      hasData: !!result.data,
      success: result.success
    });
  }
}
```

#### Logging All Responses

```typescript
export function afterRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ActionConfig
): void {
  const ctx = config.hookCtx;

  console.log('RPC response received:', {
    action: actionName,
    domain: config.domain,
    status: response.status,
    ok: response.ok,
    hasResult: result !== null,
    correlationId: ctx?.correlationId,
    timestamp: new Date().toISOString()
  });
}
```

#### Performance Timing

```typescript
export interface ActionHookContext {
  startTime?: number;
  trackPerformance?: boolean;
}

export function afterRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ActionConfig
): void {
  const ctx = config.hookCtx;

  if (ctx?.trackPerformance && ctx.startTime) {
    const duration = Date.now() - ctx.startTime;

    console.log('Performance metrics:', {
      action: actionName,
      duration: `${duration}ms`,
      status: response.status,
      success: result !== null && result.success
    });

    // Send to analytics service
    trackMetric('rpc.duration', duration, {
      action: actionName,
      status: response.status
    });
  }
}
```

#### Telemetry Tracking

```typescript
export function afterRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ActionConfig
): void {
  // Send telemetry to monitoring service
  sendTelemetry({
    event: 'rpc.request.completed',
    action: actionName,
    domain: config.domain,
    status: response.status,
    success: response.ok && result?.success,
    timestamp: Date.now()
  });
}
```

#### Error Monitoring

```typescript
export function afterRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ActionConfig
): void {
  // Track errors in error monitoring service
  if (result === null || !result.success) {
    Sentry.captureMessage('RPC request failed', {
      level: 'error',
      extra: {
        action: actionName,
        status: response.status,
        errors: result?.errors,
        url: response.url
      }
    });
  }
}
```

### Config Precedence Rules

When using `beforeRequest` hooks, the **original config passed to the action always takes precedence** over the modified config:

```typescript
export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  return {
    ...config,
    headers: {
      'X-Default-Header': 'value',
      ...config.headers  // ← Original headers override defaults
    },
    customFetch: config.customFetch || myDefaultFetch  // ← Original takes precedence
  };
}
```

**Precedence order:**
1. Original `config` values used in action (highest priority)
2. Modified config from `beforeRequest` hook
3. Default fetch implementation (lowest priority)

This ensures that per-request customizations always override hook defaults.

### Exception Handling

Hooks **do not catch exceptions** - any errors thrown by hooks will propagate to the caller:

```typescript
export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  if (!isValidConfig(config)) {
    // This exception propagates to the caller
    throw new Error('Invalid RPC configuration');
  }
  return config;
}
```

**Use Cases for Exception Propagation:**

1. **Error Boundaries** - Let framework error boundaries catch and display errors
2. **Global Error Handlers** - Centralized error handling in your app
3. **Fail-Fast Validation** - Stop execution on critical errors

```typescript
// React component with error boundary
function MyComponent() {
  const handleSubmit = async () => {
    try {
      const result = await createTodo({
        fields: ["id", "title"],
        input: {
          title: "New Todo",
          userId: "123e4567-e89b-12d3-a456-426614174000"
        },
        hookCtx: {
          correlationId: 'user-submit-action',
          trackPerformance: true
        }
      });
      // Handle success
    } catch (error) {
      // Hook threw an exception
      console.error('RPC call failed:', error);
    }
  };
}
```

### Complete Working Example

Here's a complete example showing all hook features with the simplified pattern:

```typescript
// rpcHooks.ts
import type { ActionConfig, ValidationConfig } from './generated';

// Define your custom hook context interfaces
export interface ActionHookContext {
  trackPerformance?: boolean;
  startTime?: number;
  correlationId?: string;
}

export interface ValidationHookContext {
  formId?: string;
}

// Action hooks - directly use ActionConfig (no generics needed!)
export async function beforeActionRequest(
  actionName: string,
  config: ActionConfig
): Promise<ActionConfig> {
  const ctx = config.hookCtx;

  // Add correlation ID and client version headers
  const headers: Record<string, string> = {
    'X-Client-Version': '1.0.0',
    'X-Correlation-ID': ctx?.correlationId || generateRequestId(),
    ...config.headers
  };

  // Setup timing for performance tracking
  const modifiedCtx = ctx?.trackPerformance
    ? { ...ctx, startTime: Date.now() }
    : ctx;

  console.log(`[RPC] ${actionName} started`, {
    correlationId: ctx?.correlationId
  });

  return {
    ...config,
    headers,
    ...(modifiedCtx && { hookCtx: modifiedCtx })
  };
}

function generateRequestId(): string {
  return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

export async function afterActionRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ActionConfig
): Promise<void> {
  const ctx = config.hookCtx;

  // Track timing (ctx.startTime is automatically properly typed!)
  const duration = ctx?.startTime ? Date.now() - ctx.startTime : 0;

  // Log result
  if (result === null) {
    console.error(`[RPC] ${actionName} failed:`, {
      status: response.status,
      duration: `${duration}ms`
    });
  } else {
    console.log(`[RPC] ${actionName} completed:`, {
      success: result.success,
      duration: `${duration}ms`
    });
  }
}

// Validation hooks - directly use ValidationConfig (no generics needed!)
export async function beforeValidationRequest(
  actionName: string,
  config: ValidationConfig
): Promise<ValidationConfig> {
  const ctx = config.hookCtx;
  console.log(`[Validation] ${actionName} started`, { formId: ctx?.formId });
  return config;
}

export async function afterValidationRequest(
  actionName: string,
  response: Response,
  result: any | null,
  config: ValidationConfig
): Promise<void> {
  const ctx = config.hookCtx;
  console.log(`[Validation] ${actionName} completed`, {
    formId: ctx?.formId,
    hasErrors: result && !result.success
  });
}
```

```typescript
// Usage in your application
import { createTodo, validateCreateTodo } from './ash_rpc';

// Action with hooks
const result = await createTodo({
  fields: ["id", "title", "createdAt"],
  input: {
    title: "Learn AshTypescript Hooks",
    userId: getCurrentUserId()
  },
  hookCtx: {
    trackPerformance: true,
    correlationId: 'user-create-todo-123'
  }
});

// Validation with hooks
const validationResult = await validateCreateTodo({
  input: {
    title: "Test Todo",
    userId: "123e4567-e89b-12d3-a456-426614174000"
  },
  hookCtx: {
    formId: 'create-todo-form'
  }
});
```

## Channel Lifecycle Hooks

AshTypescript provides lifecycle hooks for Phoenix Channel-based RPC actions, mirroring the HTTP hooks functionality but adapted for real-time channel communication. These hooks enable the same cross-cutting concerns (logging, telemetry, performance tracking, error monitoring) but for WebSocket-based communication instead of HTTP requests.

### Why Use Channel Lifecycle Hooks?

Channel lifecycle hooks provide a centralized way to:
- **Log channel messages** - Track channel communication for debugging
- **Measure performance** - Time channel operations and track latency
- **Send telemetry** - Report metrics to monitoring services
- **Handle errors globally** - Track channel errors in Sentry, Datadog, etc.
- **Add default configuration** - Set default timeouts or other options
- **Transform messages** - Modify config before pushing to channel

### Key Differences from HTTP Hooks

Channel hooks differ from HTTP hooks because they work with Phoenix Channel's message-based communication:

| Aspect | HTTP Hooks | Channel Hooks |
|--------|-----------|---------------|
| **Communication** | Request/Response (HTTP) | Message-based (WebSocket) |
| **API Style** | Promise-based | Callback-based |
| **Response Types** | Success or Error | ok, error, or timeout |
| **Hook Names** | `beforeRequest`, `afterRequest` | `beforeChannelPush`, `afterChannelResponse` |

### Channel Configuration

Configure channel lifecycle hooks in your application config:

```elixir
# config/config.exs
config :ash_typescript,
  # Channel-based hooks for RPC actions
  rpc_action_before_channel_push_hook: "ChannelHooks.beforeChannelPush",
  rpc_action_after_channel_response_hook: "ChannelHooks.afterChannelResponse",

  # Channel-based hooks for validation actions
  rpc_validation_before_channel_push_hook: "ChannelHooks.beforeValidationChannelPush",
  rpc_validation_after_channel_response_hook: "ChannelHooks.afterValidationChannelResponse",

  # TypeScript types for channel hook context (optional)
  rpc_action_channel_hook_context_type: "ChannelHooks.ActionChannelHookContext",
  rpc_validation_channel_hook_context_type: "ChannelHooks.ValidationChannelHookContext",

  # Import the module containing your channel hook functions
  import_into_generated: [
    %{
      import_name: "ChannelHooks",
      file: "./channelHooks"
    }
  ]
```

**Configuration Options:**

| Config | Purpose | Default |
|--------|---------|---------|
| `rpc_action_before_channel_push_hook` | Function called before channel push for RPC actions | `nil` (disabled) |
| `rpc_action_after_channel_response_hook` | Function called after channel response for RPC actions | `nil` (disabled) |
| `rpc_validation_before_channel_push_hook` | Function called before channel push for validations | `nil` (disabled) |
| `rpc_validation_after_channel_response_hook` | Function called after channel response for validations | `nil` (disabled) |
| `rpc_action_channel_hook_context_type` | TypeScript type for action channel hook context | `"Record<string, any>"` |
| `rpc_validation_channel_hook_context_type` | TypeScript type for validation channel hook context | `"Record<string, any>"` |

### Channel Hook Function Signatures

Channel hooks receive the full config object and can access the optional `hookCtx` from it.

**Important:** AshTypescript exports `ActionChannelConfig` and `ValidationChannelConfig` types from the generated file. These types automatically include your custom `hookCtx` types based on your configuration settings.

#### Configuring Custom Channel Hook Context Types

When you configure channel context type settings in your Elixir config, the generated TypeScript interfaces will automatically include these types:

```elixir
# config/config.exs
config :ash_typescript,
  # TypeScript types for channel hook context
  rpc_action_channel_hook_context_type: "ChannelHooks.ActionChannelHookContext",
  rpc_validation_channel_hook_context_type: "ChannelHooks.ValidationChannelHookContext"
```

With this configuration, the generated `ActionChannelConfig` and `ValidationChannelConfig` types will have properly typed `hookCtx` fields:

```typescript
// Generated types (in your generated file)
export interface ActionChannelConfig {
  // ... other fields ...
  hookCtx?: ChannelHooks.ActionChannelHookContext;  // ← Your custom type
}

export interface ValidationChannelConfig {
  // ... other fields ...
  hookCtx?: ChannelHooks.ValidationChannelHookContext;  // ← Your custom type
}
```

#### Implementing Channel Hook Functions

Simply import and use the generated config types directly - no generics needed!

```typescript
// channelHooks.ts - Define your custom hook context interfaces
export interface ActionChannelHookContext {
  correlationId?: string;
  trackPerformance?: boolean;
  startTime?: number;
}

export interface ValidationChannelHookContext {
  formId?: string;
  validationLevel?: "strict" | "normal";
}

// Import the generated config types
import type { ActionChannelConfig, ValidationChannelConfig } from './generated';

// Implement your channel hook functions - the hookCtx is already properly typed!
export async function beforeChannelPush(
  actionName: string,
  config: ActionChannelConfig
): Promise<ActionChannelConfig> {
  const ctx = config.hookCtx;

  // ctx is automatically typed as ActionChannelHookContext | undefined
  if (ctx?.trackPerformance) {
    const modifiedCtx = { ...ctx, startTime: Date.now() };
    return { ...config, hookCtx: modifiedCtx };
  }

  return config;
}

export async function afterChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,  // result (for ok), error (for error), or null (for timeout)
  config: ActionChannelConfig
): Promise<void> {
  const ctx = config.hookCtx;

  // ctx.startTime is properly typed (no type assertion needed!)
  if (ctx?.trackPerformance && ctx.startTime) {
    const duration = Date.now() - ctx.startTime;
    console.log(`[Channel] ${actionName} took ${duration}ms`);
  }
}

// Similarly for validation channel hooks
export async function beforeValidationChannelPush(
  actionName: string,
  config: ValidationChannelConfig
): Promise<ValidationChannelConfig> {
  const ctx = config.hookCtx;

  if (ctx?.validationLevel === "strict") {
    console.log(`[Channel Validation] Strict mode enabled`);
  }

  return config;
}
```

**Key Benefits:**
- **Type safety** - Your custom context fields are properly typed automatically
- **IntelliSense** - IDE autocomplete works for your custom fields
- **No generics needed** - The generated types already include your context types
- **Simpler code** - Direct usage without complex generic constraints

#### Channel Config Structure

The generated `ActionChannelConfig` and `ValidationChannelConfig` interfaces include all available configuration fields:

```typescript
// Generated ActionChannelConfig interface (in your generated file)
export interface ActionChannelConfig {
  // Channel connection (required)
  channel: Channel;

  // Request parameters (varies by action)
  input?: Record<string, any>;
  primaryKey?: any;
  fields?: Array<string | Record<string, any>>;
  filter?: Record<string, any>;
  sort?: string;
  page?: { limit?: number; offset?: number; count?: boolean };

  // Metadata
  metadataFields?: Record<string, any>;

  // Channel options
  timeout?: number;

  // Handlers (required for channel operations)
  resultHandler: (result: any) => void;
  errorHandler?: (error: any) => void;
  timeoutHandler?: () => void;

  // Multitenancy
  tenant?: string;

  // Hook context (automatically typed based on your config)
  hookCtx?: YourActionChannelHookContext;
}
```

**Key Points:**
- Channel hooks support async operations (Promise-based)
- `beforeChannelPush` receives action name and config, returns modified config
- `afterChannelResponse` receives action name, response type, data, and config
- Response type distinguishes between three channel outcomes: "ok", "error", "timeout"
- Original config takes precedence over modified config
- Your custom `hookCtx` type is automatically included when you configure context type settings

### beforeChannelPush Hook

The `beforeChannelPush` hook runs **before the channel.push()** call and can modify the channel message configuration. Common use cases:

#### Setting Default Timeout

```typescript
// channelHooks.ts
export interface ActionChannelHookContext {
  useDefaultTimeout?: boolean;
  customTimeout?: number;
}

export async function beforeChannelPush(
  actionName: string,
  config: ChannelActionConfig
): Promise<ChannelActionConfig> {
  const ctx = config.hookCtx;

  // Set default timeout if not specified
  if (ctx?.useDefaultTimeout && !config.timeout) {
    return {
      ...config,
      timeout: ctx.customTimeout || 10000  // 10 second default
    };
  }

  return config;
}
```

```typescript
// Usage: Pass timeout preferences via hook context
listTodosChannel({
  channel: myChannel,
  fields: ["id", "title"],
  hookCtx: {
    useDefaultTimeout: true,
    customTimeout: 15000
  },
  resultHandler: (result) => console.log(result)
});
```

#### Logging Channel Messages

```typescript
export interface ActionChannelHookContext {
  correlationId?: string;
  trackPerformance?: boolean;
  startTime?: number;
}

export async function beforeChannelPush(
  actionName: string,
  config: ChannelActionConfig
): Promise<ChannelActionConfig> {
  const ctx = config.hookCtx;

  // Setup timing
  if (ctx?.trackPerformance && ctx) {
    ctx.startTime = Date.now();
  }

  console.log(`[Channel] Pushing to channel:`, {
    action: actionName,
    correlationId: ctx?.correlationId,
    timestamp: new Date().toISOString()
  });

  return config;
}
```

### afterChannelResponse Hook

The `afterChannelResponse` hook runs **after the channel response is received** (ok, error, or timeout) and is used for side effects. It receives four parameters:

1. `actionName: string` - The name of the action being executed
2. `responseType: "ok" | "error" | "timeout"` - The type of channel response
3. `data: any` - Response data (result for "ok", error for "error", null for "timeout")
4. `config: ChannelActionConfig` - The config used for the request

#### Logging All Channel Responses

```typescript
export async function afterChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,
  config: ChannelActionConfig
): Promise<void> {
  const ctx = config.hookCtx;

  console.log(`[Channel] Response received:`, {
    action: actionName,
    responseType,
    hasData: data !== null,
    correlationId: ctx?.correlationId,
    timestamp: new Date().toISOString()
  });

  // Log specific details based on response type
  if (responseType === "error") {
    console.error(`[Channel] Error in ${actionName}:`, data);
  } else if (responseType === "timeout") {
    console.warn(`[Channel] Timeout in ${actionName}`);
  }
}
```

#### Performance Timing

```typescript
export interface ActionChannelHookContext {
  startTime?: number;
  trackPerformance?: boolean;
  correlationId?: string;
}

export async function afterChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,
  config: ChannelActionConfig
): Promise<void> {
  const ctx = config.hookCtx;

  if (ctx?.trackPerformance && ctx.startTime) {
    const duration = Date.now() - ctx.startTime;

    console.log(`[Channel] Performance metrics:`, {
      action: actionName,
      duration: `${duration}ms`,
      responseType,
      success: responseType === "ok" && data?.success,
      correlationId: ctx?.correlationId
    });

    // Send to analytics service
    trackMetric('channel.rpc.duration', duration, {
      action: actionName,
      responseType,
      success: responseType === "ok"
    });
  }
}
```

#### Telemetry Tracking

```typescript
export async function afterChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,
  config: ChannelActionConfig
): Promise<void> {
  // Send telemetry to monitoring service
  sendTelemetry({
    event: 'channel.rpc.completed',
    action: actionName,
    domain: config.domain,
    responseType,
    success: responseType === "ok" && data?.success,
    timestamp: Date.now()
  });

  // Track specific response types
  if (responseType === "timeout") {
    sendTelemetry({
      event: 'channel.rpc.timeout',
      action: actionName,
      timestamp: Date.now()
    });
  }
}
```

#### Error Monitoring

```typescript
export async function afterChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,
  config: ChannelActionConfig
): Promise<void> {
  // Track errors in error monitoring service
  if (responseType === "error" || responseType === "timeout") {
    Sentry.captureMessage('Channel RPC failed', {
      level: 'error',
      extra: {
        action: actionName,
        responseType,
        data: responseType === "error" ? data : null,
        domain: config.domain
      }
    });
  } else if (data && !data.success) {
    // Track validation errors from successful channel responses
    Sentry.captureMessage('Channel RPC validation error', {
      level: 'warning',
      extra: {
        action: actionName,
        errors: data.errors
      }
    });
  }
}
```

### Channel Config Precedence Rules

When using `beforeChannelPush` hooks, the **original config always takes precedence** over the modified config:

```typescript
export async function beforeChannelPush(
  actionName: string,
  config: ChannelActionConfig
): Promise<ChannelActionConfig> {
  return {
    ...config,
    timeout: config.timeout ?? 10000  // ← Original timeout takes precedence
  };
}
```

**Precedence order:**
1. Original `config` values (highest priority)
2. Modified config from `beforeChannelPush` hook
3. No default timeout (lowest priority)

This ensures that per-request customizations always override hook defaults.

### Complete Channel Working Example

Here's a complete example showing all channel hook features with the simplified pattern:

```typescript
// channelHooks.ts
import type { ActionChannelConfig, ValidationChannelConfig } from './generated';

// Define custom hook context interfaces
export interface ActionChannelHookContext {
  trackPerformance?: boolean;
  startTime?: number;
  correlationId?: string;
}

export interface ValidationChannelHookContext {
  formId?: string;
  validationLevel?: "strict" | "normal";
}

// Action hooks - directly use ActionChannelConfig (no generics needed!)
export async function beforeChannelPush(
  actionName: string,
  config: ActionChannelConfig
): Promise<ActionChannelConfig> {
  const ctx = config.hookCtx;

  // Setup timing - properly update context immutably
  const modifiedCtx = ctx?.trackPerformance
    ? { ...ctx, startTime: Date.now() }
    : ctx;

  console.log(`[Channel] ${actionName} starting`, {
    correlationId: ctx?.correlationId
  });

  return {
    ...config,
    ...(modifiedCtx && { hookCtx: modifiedCtx })
  };
}

export async function afterChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,
  config: ActionChannelConfig
): Promise<void> {
  const ctx = config.hookCtx;

  // Track timing - ctx.startTime is automatically properly typed!
  const duration = ctx?.startTime ? Date.now() - ctx.startTime : 0;

  // Log result
  console.log(`[Channel] ${actionName} completed:`, {
    responseType,
    duration: `${duration}ms`,
    correlationId: ctx?.correlationId
  });

  // Track errors
  if (responseType !== "ok") {
    console.error(`[Channel] ${actionName} failed:`, { responseType, data });
  }
}

// Validation hooks - directly use ValidationChannelConfig (no generics needed!)
export async function beforeValidationChannelPush(
  actionName: string,
  config: ValidationChannelConfig
): Promise<ValidationChannelConfig> {
  const ctx = config.hookCtx;
  console.log(`[Channel Validation] ${actionName} started`, {
    formId: ctx?.formId,
    validationLevel: ctx?.validationLevel
  });
  return config;
}

export async function afterValidationChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,
  config: ValidationChannelConfig
): Promise<void> {
  const ctx = config.hookCtx;
  console.log(`[Channel Validation] ${actionName} completed`, {
    formId: ctx?.formId,
    responseType,
    hasErrors: responseType === "ok" && data && !data.success
  });
}
```

```typescript
// Usage in your application
import { listTodosChannel, createTodoChannel, validateCreateTodoChannel } from './ash_rpc';
import { Channel } from "phoenix";

// Action with channel hooks
listTodosChannel({
  channel: myChannel,
  fields: ["id", "title", { user: ["name"] }],
  hookCtx: {
    trackPerformance: true,
    correlationId: 'list-todos-123'
  },
  resultHandler: (result) => {
    if (result.success) {
      console.log("Todos loaded:", result.data);
    }
  }
});

// Validation with channel hooks
validateCreateTodoChannel({
  channel: myChannel,
  input: {
    title: "Test Todo",
    userId: "123e4567-e89b-12d3-a456-426614174000"
  },
  hookCtx: {
    formId: 'create-todo-form',
    validationLevel: 'strict'
  },
  resultHandler: (result) => {
    if (!result.success) {
      console.log("Validation errors:", result.errors);
    }
  }
});
```

## Troubleshooting

### HTTP Hooks

**Config precedence not working:**
```typescript
// ❌ Wrong: Original config gets overridden
return {
  headers: { ...config.headers, 'X-Custom': 'value' },
  ...config
};

// ✅ Correct: Original config takes precedence
return {
  ...config,
  headers: { 'X-Custom': 'value', ...config.headers }
};
```

**Performance timing not working:**
```typescript
// ❌ Wrong: Context is read-only, modifications lost
export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  const ctx = config.hookCtx;
  ctx.startTime = Date.now();  // Lost!
  return config;
}

// ✅ Correct: Return modified context
export function beforeRequest(actionName: string, config: ActionConfig): ActionConfig {
  const ctx = config.hookCtx || {};
  return {
    ...config,
    hookCtx: { ...ctx, startTime: Date.now() }
  };
}
```

**Hook not executing:**
- Verify hook functions are exported from the configured module
- Check that `import_into_generated` includes the hooks module
- Regenerate types with `mix ash.codegen --dev`
- Ensure hook function names match the configuration exactly

**TypeScript errors with hook context:**
```typescript
// ❌ Wrong: Type assertion on config
const ctx = config.hookCtx as ActionHookContext;
ctx.trackPerformance;  // Error if hookCtx is undefined

// ✅ Correct: Optional chaining or type guard
const ctx = config.hookCtx as ActionHookContext | undefined;
if (ctx?.trackPerformance) {
  // Safe to use
}
```

### Channel Hooks

**Config precedence not working:**
```typescript
// ❌ Wrong: Original config gets overridden
return {
  timeout: 10000,
  ...config
};

// ✅ Correct: Original config takes precedence
return {
  ...config,
  timeout: config.timeout ?? 10000
};
```

**Hook not executing:**
- Verify channel hook functions are exported from the configured module
- Check that `import_into_generated` includes the channel hooks module
- Regenerate types with `mix ash.codegen --dev`
- Ensure hook function names match the configuration exactly
- Verify that `generate_phx_channel_rpc_actions: true` is set in config

**TypeScript errors with channel hook context:**
```typescript
// ❌ Wrong: Type assertion without null check
const ctx = config.hookCtx as ActionChannelHookContext;
ctx.trackPerformance;  // Error if hookCtx is undefined

// ✅ Correct: Optional chaining or type guard
const ctx = config.hookCtx as ActionChannelHookContext | undefined;
if (ctx?.trackPerformance) {
  // Safe to use
}
```

**Response type not being handled:**
```typescript
// ✅ Handle all three response types
export async function afterChannelResponse(
  actionName: string,
  responseType: "ok" | "error" | "timeout",
  data: any,
  config: any
): Promise<void> {
  switch (responseType) {
    case "ok":
      // Handle successful response
      break;
    case "error":
      // Handle error response
      break;
    case "timeout":
      // Handle timeout response
      break;
  }
}
```