Portal Community

The BaseHook Class

BaseHook is the recommended base class. It provides default implementations for the logging helpers defined in IHookActivityLogger and ensures your hook satisfies the full IHook contract:

import { BaseHook, IPipelineContext, HookResult } from 'edge-stream-js';

export class MyCustomHook extends BaseHook {
  readonly name = 'MyCustomHook';
  readonly priority = 150;  // choose based on when it should run

  // Dependencies injected via constructor
  constructor(private config: { allowedTopics: string[] }) {
    super();
  }

  async execute(context: IPipelineContext): Promise<HookResult> {
    const topic = context.envelope.meta.topic || '';

    // Example: filter by allowed topics
    if (!this.config.allowedTopics.some(t => topic.startsWith(t))) {
      return { continue: false }; // drop silently
    }

    // Enrich context metadata for downstream hooks
    context.metadata.processedByMyHook = true;
    context.addActivity(`MyCustomHook processed topic: ${topic}`);

    return { continue: true };
  }
}

Typed Context Hook

For hooks that need domain-specific context data, extend IPipelineContext:

import { IHook, IPipelineContext, HookResult } from 'edge-stream-js';

interface IAuthContext extends IPipelineContext {
  readonly userId: string;
  readonly tenantId: string;
  readonly roles: string[];
}

export class RoleAuthorizationHook implements IHook<IAuthContext> {
  readonly name = 'RoleAuthorizationHook';
  readonly priority = 40;

  private readonly requiredRole: string;

  constructor(requiredRole: string) {
    this.requiredRole = requiredRole;
  }

  async execute(context: IAuthContext): Promise<HookResult> {
    if (!context.roles.includes(this.requiredRole)) {
      context.abort(`Access denied: requires role '${this.requiredRole}'`);
      return { continue: false };
    }
    return { continue: true };
  }
}

Complete Hook Checklist

1

Choose a priority

Pick a number that places your hook in the right stage. Use 5 for observability, 90–110 for security/normalization, 120–199 for enrichment, 200–299 for processing, 300+ for post-processing.

2

Define a unique name

The name property is used by HooksMonitor and in all log output. Use PascalCase with "Hook" suffix: 'TenantValidationHook'.

3

Implement execute()

Always return a HookResult. Never throw — catch exceptions internally and decide whether to abort or continue. Return { continue: false } to drop the message.

4

Register before start()

Call server.incomingPipeline.addHook(new MyCustomHook(...)) before edgeStream.start().

5

Test in isolation

Hooks are plain classes — unit test them by creating a mock IPipelineContext and calling execute() directly.

Testing a Hook

import { describe, it, expect } from 'vitest';
import { MyCustomHook } from './MyCustomHook';

describe('MyCustomHook', () => {
  it('should drop messages not in allowed topics', async () => {
    const hook = new MyCustomHook({ allowedTopics: ['workflow.', 'agent.'] });

    const mockContext = {
      envelope: { meta: { topic: 'other.event', id: 'test-123', serverId: 'bas' } },
      body: {},
      metadata: {},
      isAborted: false,
      isPaused: false,
      activityLog: [],
      abort: vi.fn(),
      pause: vi.fn(),
      addActivity: vi.fn(),
      clearActivityLog: vi.fn(),
    };

    const result = await hook.execute(mockContext as any);
    expect(result.continue).toBe(false);
  });

  it('should pass through allowed topics', async () => {
    const hook = new MyCustomHook({ allowedTopics: ['workflow.'] });
    const mockContext = {
      envelope: { meta: { topic: 'workflow.started', id: 'test-456', serverId: 'bas' } },
      metadata: {},
      addActivity: vi.fn(),
      ...otherMocks
    };

    const result = await hook.execute(mockContext as any);
    expect(result.continue).toBe(true);
    expect(mockContext.metadata.processedByMyHook).toBe(true);
  });
});
Hooks Are Stateful Hooks are instantiated once and reused for every message. You can maintain state (counters, caches, dedup sets) inside the hook class. This is intentional — design your hook to be thread-safe if it holds mutable state that async execution could race on.