Architecture & concepts

Kinesis.js is organised into three layers with one responsibility each, connected by a single rAF-driven tick pipeline. Understanding that shape explains every option in the API.

Three layers, one responsibility each

Each layer is a separate, independently-versioned package. You only depend on the ones your stack needs.

LayerOwnsExamples
1 · CoreTime, math, memory, events. The interpolation engine itself.@kinesisjs/core
2 · AdapterMap feature lifecycle, styling, trails, viewport.@kinesisjs/openlayers · @kinesisjs/leaflet
3 · WrapperReactive bindings and framework-managed teardown.@kinesisjs/angular

The core never imports a map library, and an adapter never imports a framework. That boundary is what makes Kinesis.js map-library agnostic and framework agnostic at the same time.

The data pipeline

Positions flow one way. Your feed calls ingest(); the clock drives interpolation; the adapter writes to the map.

// every frame, for every vehicle ingest(positions) -> per-vehicle slot { previous, current } -> Clock (rAF, ~60fps) -> renderTime = now - renderLagMs -> ratio = (renderTime - prev.receivedAt) / period -> point = interpolate(prev, curr, ratio) -> adapter.updatePosition(id, point)

The clock

The tick loop is built on requestAnimationFrame, not setInterval. That matters for two reasons:

  • Frame-synchronous. Updates land exactly when the browser is about to paint, so there's no double-buffering jitter.
  • Tab-aware. rAF pauses in background tabs, so the engine never builds up a backlog and never produces a catch-up jump when you return.

Memory management

Each vehicle owns a fixed ring slot holding only the points it needs — previous and current (plus previous2 for the smooth mode). Ingesting a new position shifts the slot rather than allocating, so the hot path is allocation-free and memory stays flat across an 8-hour shift regardless of how many updates arrive.

Bounded by design

Stale vehicles are swept out automatically, and trail rendering uses a capped ring buffer (maxPoints). There is no unbounded history to leak.

Lifecycle

The Sweeper watches each vehicle's lastIngestAt and moves it through a small state machine, emitting an event at every transition.

StateEnters whenEvent
activeReceiving updates normally.vehicleadded
warningIdle past warningThreshold (60s).vehiclewarning
staleIdle past staleThreshold (10min) → removed.vehiclestale
completedYou call markCompleted(id) (end of shift).vehiclecompleted

An adapter can optionally render these states (for example dimming a warning vehicle via warningOpacity) by implementing setVehicleState().

Design principles

  • Narrow scope, deep quality. One job — interpolation — done to production depth, not a kitchen-sink mapping toolkit.
  • Zero-cost abstractions. The tick loop is allocation-free; adapters add a thin styling layer, nothing more.
  • Production defaults. Sanity checks, thresholds and throttling work out of the box.
  • TypeScript-first, JavaScript-friendly. Full types, but no build step required to consume.
  • Open to new adapters. The TrackAdapter interface is ~6 methods; writing one is an afternoon.