Skip to content

Event Boundaries

Problem

External input needs logging and authorization, but Clockwork has no interceptor pipeline.

Solution

Perform transport validation and authorization at the input boundary, then send accepted events to the actor.

ts
import { defineMachine } from '@vielzeug/clockwork';

type Event = { type: 'START' } | { type: 'RESET' };
const machine = defineMachine<Record<string, never>, Event>()({
  initial: 'idle',
  states: {
    idle: { on: { START: { target: 'active' } } },
    active: { on: { RESET: { target: 'idle' } } },
  },
});

const actor = machine.createActor();
const isEvent = (event: unknown): event is Event =>
  typeof event === 'object' && event !== null &&
  'type' in event && ((event as { type: unknown }).type === 'START' || (event as { type: unknown }).type === 'RESET');

const dispatch = (event: unknown, authorized: boolean) => {
  if (!isEvent(event)) return 'invalid';
  if (event.type === 'RESET' && !authorized) return 'denied';
  actor.send(event);
  return 'sent';
};

console.log(dispatch({ type: 'START' }, true)); // 'sent'
console.log(dispatch({ type: '__proto__' }, true)); // 'invalid'
console.log(dispatch({ type: 'RESET' }, false)); // 'denied'
console.log(actor.snapshot.state); // 'active'
actor.dispose();

Pitfalls

  • Put state-dependent admission rules in guard, not in a generic boundary.
  • Do not add an interceptor abstraction around every actor without a concrete boundary need.