Architecture
Layers, scheduling, persistence boundary, and extension points.
Scope
Stable domain of this package:
- universe/priority output addresses
- 512-slot frames and sparse one-based channel writes
- linear fades (shared or per-channel)
- active-rate sends and idle keepalives
- selected-universe packet viewing
Out of scope: fixtures, personalities, attributes, scenes, cues, show control, patching, licensing. A higher-level library can resolve those into frames and call a source.
Layers
Application / fixture layer
|
SacnSource + Universe
/ \
output engine output store
| / \
transport memory Redis
|
Node sACN transport
Node receiver → ViewerService → callbacks / streams
|
viewer store
Host Elysia app → HTTP adapter → source + viewerThe root module does not open sockets or read environment variables at import time. Adapters are replaceable behind small contracts.
Scheduling
Each (universe, priority) pair is an independent output. State includes the
current interpolated frame, target frame, last sent frame, transitions,
sequence, identity, and next due time. Universe handles are a cached view — they
do not own separate engine state.
Dirty or fading outputs run at activeFps (default 44). Stable outputs send
keepalives at idleFps (default 2). Fades use a monotonic clock; persisted
updatedAt is not the interpolation clock.
Mutations are ordered through the source. Transport sends are serialized per output so a timeout cannot overlap another send, and a clear cannot be followed by a stale frame.
Persistence boundary
Durable intent only: address, CID, source suffix, idle rate, target frame, update time. Runtime-only: interpolation, sequence, dirty flags, last-send telemetry.
On restore, the engine resumes toward the persisted target. Stores are invoked in mutation order. Redis (and other trust-boundary adapters) validate records on load.
Viewer backpressure
The receiver normalizes packets; the viewer filters by selection and pads to 512 slots. Callbacks are synchronous and isolated. Streams coalesce by universe and drop oldest-at-capacity. Reception never waits on slow consumers.
HTTP boundary
The Elysia plugin is routes only. Binding, TLS, CORS, auth, rate limits, and
process signals stay in the host. Compatibility /outputs… routes are migration
aids with deprecation metadata.
Extension points
Implement root contracts:
OutputTransport— another sender, recorder, or gatewayOutputStore/ViewerStore— another backendReceiver— another packet sourceClock— deterministic testsLogger— host observability
Custom HTTP should depend on SacnSourceContract, not engine internals. Future
fixture packages should generate frames without expanding this domain.
subscribe on the source exposes lifecycle and output events without leaking
mutable scheduler state.