Core Concepts¶
Architecture Overview¶
graph TD
S1[FileWatcherSource] -->|emit| B[SignalBus]
S2[CronSource] -->|emit| B
S3[ClockSource] -->|emit| B
B --> MW[Middleware Pipeline]
MW --> H1[Type Handlers]
MW --> H2[Any Handlers]
B --> T[Transport]
B --> ST[Store]
B --> E[Executor]
T -.->|default| MT[MemoryTransport]
ST -.->|default| NS[NoopStore]
E -.->|default| SE[SequentialExecutor]
Cadence has five core abstractions. Each is a pluggable interface with a sensible default.
Signal¶
A signal is a typed event with a standard shape:
interface BaseSignal<T extends string = string, P = unknown> {
type: T; // Signal type identifier
ts: number; // Unix timestamp (ms)
id: string; // Unique ID
source?: string; // Origin identifier
payload: P; // Signal-specific data
}
You define your own signal types using DefineSignals:
type MySignals = DefineSignals<{
"file.changed": { path: string };
"cron.fired": { jobId: string };
}>;
// Result: BaseSignal<"file.changed", { path: string }>
// | BaseSignal<"cron.fired", { jobId: string }>
Bus¶
The signal bus routes signals to handlers. It coordinates transport, store, and executor:
interface SignalBus<S extends BaseSignal> {
emit(signal: S): Promise<void>;
on<T extends S["type"]>(type: T, handler: SignalHandler<S, T>): () => void;
onAny(handler: AnySignalHandler<S>): () => void;
use(middleware: Middleware<S>): void;
clear(): void;
stats(): BusStats;
replay(): Promise<number>;
}
When you emit(), the bus saves to the store, dispatches through the transport, runs the middleware chain, then executes matching handlers via the executor.
Source¶
Sources produce signals from external events. Every source follows the same pattern:
interface Source<S extends BaseSignal> {
name: string;
start(emit: (signal: S) => Promise<void>): Promise<void>;
stop(): Promise<void>;
}
Built-in sources: createFileWatcherSource (file changes) and createCronSource (schedules). The createClockSource adapter converts any Clock into a Source.
Clock¶
Clocks are lower-level timing primitives. They tick at a rate and call a handler:
interface Clock {
start(handler: TickHandler): void;
stop(): void;
now(): number;
stats(): TickStats;
readonly running: boolean;
readonly seq: number;
}
Three implementations:
- IntervalClock — Production timer with backpressure policies
- TestClock — Deterministic virtual time for testing
- BridgeClock — Adapts external heartbeats into ticks
Pluggable Layers¶
Every layer has an interface and a default implementation:
| Layer | Interface | Default | Purpose |
|---|---|---|---|
| Transport | Transport<S> |
MemoryTransport |
How signals move between emitter and subscribers |
| Store | SignalStore<S> |
NoopStore |
Persistence for durability and replay |
| Executor | HandlerExecutor<S> |
SequentialExecutor |
Controls handler concurrency |
You can swap any layer independently:
const bus = createSignalBus<MySignals>({
transport: createMemoryTransport(), // or your Redis transport
store: createNoopStore(), // or your SQLite store
executor: createSequentialExecutor(), // or your concurrent executor
});
Design Philosophy¶
- Domain-agnostic — Cadence is infrastructure. It has no concept of files, cron, or AI. Consumers define signal types and sources that make sense for their domain.
- Build for the sophisticated case, implement simple — Every interface supports advanced use cases (distributed transport, durable store, concurrent execution). The defaults are the simplest possible implementation.
- Pluggable, not configurable — Instead of options flags, swap entire implementations. This keeps interfaces small and behavior predictable.