Skip to main content

Handlers

Handlers are content-context message endpoints. They receive actions routed from background and can subscribe to multicast events.

Generate a Handler

hexa generate handler tabs --namespace tabs

Then attach the handler to a specific content entry class:

hexa add handler TabsHandler MyContentEntry

@Handle must be unique by full route key. You cannot declare two handles that resolve to the same namespace:handle path anywhere in the content context.

Contents: [MyContentEntry] binds the handler to specific content entry classes. This is different from background context: background runs as a single script, while content can have multiple entries/scripts. Because of that, content handlers should declare Contents so routing and bundling stay scoped to the right content script(s).

Boundary policy decorators

Handlers follow the same route boundary model as controllers:

  • Default: internal-only.
  • @AllowExternal(...) opts a class or method into external callers.
  • @InternalOnly() can re-lock a method on an externally allowed class.
import { AllowExternal, InternalOnly } from '@hexajs-dev/common';
import { Handle, Handler } from '@hexajs-dev/core';

@AllowExternal({ ids: ['trusted.extension.id'] })
@Handler({ namespace: 'sync', Contents: [MyContentEntry] })
export class SyncHandler {
@Handle('public')
onPublic(payload: unknown): { ok: true } {
return { ok: true };
}

@InternalOnly()
@Handle('private')
onPrivate(payload: unknown): { ok: true } {
return { ok: true };
}
}

Method-level @AllowExternal() is also supported for handlers:

import { AllowExternal, InternalOnly } from '@hexajs-dev/common';
import { Handle, Handler } from '@hexajs-dev/core';

@InternalOnly()
@Handler({ namespace: 'sync', Contents: [MyContentEntry] })
export class SyncHandler {
@Handle('private')
onPrivate(payload: unknown): { ok: true } {
return { ok: true };
}

@AllowExternal()
@Handle('admin')
onAdmin(payload: unknown): { ok: true } {
return { ok: true };
}
}

Important limitation: runtime.onMessageExternal is not available in content scripts. External callers should target background first, then background can relay to content routes when needed.

Handler Example

import { Handler, Handle } from '@hexajs-dev/core';
import { HexaContentStore } from '@hexajs-dev/core';
import { LoggerService } from '../services/logger.service';
import { MyContentEntry } from './content';
import { ContentState } from './store/content.state';
import { backgroundCalled } from './store/content.actions';

@Handler({ namespace: 'tabs', Contents: [MyContentEntry] })
export class TabsHandler {
constructor(private logger: LoggerService, private store: HexaContentStore<ContentState>) {}

@Handle('active')
onActive(payload: { tabId: number }): { ok: boolean } {
this.logger.log('Background returned active tab id:', payload.tabId);
this.store.dispatch(backgroundCalled({ message: `tab ${payload.tabId}`, timestamp: Date.now() }));
return { ok: true };
}
}

Multicast Subscriptions

Use @Subscribe when one content handler listens to events pushed from background:

import { Handler, Subscribe } from '@hexajs-dev/core';
import { LoggerService } from '../services/logger.service';
import { MyContentEntry } from './content';

@Handler({ namespace: 'audit', Contents: [MyContentEntry] })
export class AuditHandler {
constructor(private logger: LoggerService) {}

@Subscribe('high-risk')
onHighRisk(payload: { message: string }): void {
this.logger.warn('Audit event received:', payload.message);
}
}

Notes

  • Handlers are content-only classes and should inject content/general services.
  • @Handle is unicast. Only one handler can exist for a given namespace:handle route.
  • Prefer services and store abstractions in handlers, not direct browser APIs.
  • Use HexaContentStore<T> + dispatch(...) to reflect routed messages into local content state.
  • Namespace plus method decorator name forms the final route key: namespace:name.
  • Pair handlers with Browser-Agnostic Messaging docs for request/response routing patterns.

@Handle

For Request/Response (Unary). CLI should enforce that handleName is UNIQUE per context.

import { Handle } from '@hexajs-dev/core';
@Handle(handleName: string)

@Handler

import { Handler } from '@hexajs-dev/core';
@Handler(options: HandlerOptions)

@Subscribe

For Fire-and-Forget (Multicast). Multiple methods can listen to the same eventName.

import { Subscribe } from '@hexajs-dev/core';
@Subscribe(eventName: string)

Supporting Types

HandlerOptions

interface HandlerOptions {
namespace: string;
Contents?: ContentClass[];
}