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 Ports 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[];
}