Changelog

Every release of the Kinesis.js packages, newest first — pulled straight from the library’s published versions.

@kinesisjs/route-awarev0.1.42026-07-11
  • #28 Harden the OSRM client: a malformed routing profile is now rejected before the request is issued (defense-in-depth on top of the existing encodeURIComponent escaping). The guard is syntactic ([A-Za-z0-9_-]+), so custom self-host profile names still work — only obviously-invalid values are rejected, surfacing as an INTERPOLATION_ERROR event instead of a doomed network call.
@kinesisjs/corev0.5.12026-06-15
  • #24 fix: harden against untrusted input
    • leaflet: the marker divIcon HTML now coerces numeric options (heading/speed/iconSize/…) to finite numbers and escapes interpolated icon/color values, so a malformed feed or crafted style option can no longer break out of an HTML attribute (DOM-XSS hardening).
    • core: non-finite heading/speed are dropped on ingest, so malformed feed values never reach a render adapter.
    • route-aware: the OSRM baseUrl must now be an http(s) URL and the routing profile is encodeURIComponent-escaped before being placed in the request URL.
@kinesisjs/angularv0.5.12026-06-15
@kinesisjs/openlayersv0.2.62026-06-15
@kinesisjs/leafletv0.1.32026-06-15
  • #24 fix: harden against untrusted input

    • leaflet: the marker divIcon HTML now coerces numeric options (heading/speed/iconSize/…) to finite numbers and escapes interpolated icon/color values, so a malformed feed or crafted style option can no longer break out of an HTML attribute (DOM-XSS hardening).
    • core: non-finite heading/speed are dropped on ingest, so malformed feed values never reach a render adapter.
    • route-aware: the OSRM baseUrl must now be an http(s) URL and the routing profile is encodeURIComponent-escaped before being placed in the request URL.
  • Updated dependencies []:

@kinesisjs/route-awarev0.1.32026-06-15
  • #24 fix: harden against untrusted input

    • leaflet: the marker divIcon HTML now coerces numeric options (heading/speed/iconSize/…) to finite numbers and escapes interpolated icon/color values, so a malformed feed or crafted style option can no longer break out of an HTML attribute (DOM-XSS hardening).
    • core: non-finite heading/speed are dropped on ingest, so malformed feed values never reach a render adapter.
    • route-aware: the OSRM baseUrl must now be an http(s) URL and the routing profile is encodeURIComponent-escaped before being placed in the request URL.
  • Updated dependencies []:

@kinesisjs/corev0.5.02026-06-12
  • #18 Add TrackerOptions.playout — a per-vehicle queue that decouples display rate from arrival rate, so feeds with variable-period ingest (jitter, replay scrubbing, retry storms) render at a steady pace instead of speeding up and slowing down with each segment.

    Opt-in and non-breaking: without playout, Tracker uses the existing classical real-time path; behaviour is byte-for-byte identical to v0.4.

    Two forms:

    • Manualplayout: { pace, bufferMs, maxQueue? } when you know your feed's worst-case gap. Pick bufferMs ≥ worstCaseGap to avoid the queue underrunning (which would freeze the marker).
    • Autoplayout: 'auto'. Tracker measures the last ~10 ingest gaps per vehicle and sets pace = avg, bufferMs = max × 1.5. Behaves classically while gathering its first 5 samples, then engages playout. Each vehicle calibrates independently, so mixed fleets (1 Hz dispatch + jittery IoT) coexist cleanly.

    Trade-off: bufferMs of additional perceived latency for smooth motion. For most fleet/dispatch use cases (where "the marker is 2 s behind" beats "the marker stutters") this is the right exchange. Stable 1 Hz feeds shouldn't enable it.

    Composition: works on top of every interpolation mode. Pairing with 'smooth' (3-point Catmull-Rom) yields the maximum-pleasant render path for jittery feeds — smooth shapes the geometry, playout flattens the rhythm.

    Also adds PlayoutOptions and PlayoutQueueEntry to the public type surface.

@kinesisjs/angularv0.5.02026-06-12
  • #22 Expose [playout] @Input on KinesisMapDirective, mirroring the TrackerOptions.playout field that landed in @kinesisjs/[email protected].

    <div kinesisMap [positions]="positions" [interpolation]="'smooth'" [playout]="'auto'"></div>
    

    Forwarded to the underlying Tracker only when set, so omitting the input keeps the classical real-time path. Use 'auto' for unknown feeds (Tracker self-calibrates from the gap history) or { pace, bufferMs, maxQueue } when you know your worst-case gap.

@kinesisjs/corev0.4.02026-06-12
  • #16 Add interpolation: 'smooth' — a 3-point centripetal Catmull-Rom mode for jitter and variable-period feeds.

    The Tracker now keeps a third historical point per vehicle (previous2) and routes smooth-mode ticks through a cubic spline over previous2 → previous → current, with a mirror phantom for the trailing tangent. The marker glides through each waypoint instead of kinking, which is especially visible on irregular feeds (random arrival times, dead reckoning, replay scrubbing).

    Opt-in and conservative:

    • Default stays 'linear'. Existing apps see no behavioural change.
    • Until the third ingest lands the spline falls back to linear — no spurious motion from incomplete history.
    • If the previous2 → previous gap exceeds maxInterpolationGap, that control point is dropped (stale data shouldn't warp the curve).
    • All sanity checks (anomalous jump, sharp turn, render-lag warm-up) and the existing custom-interpolator path are untouched.

    Also exposes catmullRomLerp as a public math helper, alongside linearLerp / haversineDistance / shortestArcDiff, for authors of custom interpolators who want the same smoothing primitive.

@kinesisjs/angularv0.4.02026-06-12
  • #20 Expose [playout] @Input on KinesisMapDirective, mirroring the TrackerOptions.playout field that landed in @kinesisjs/[email protected].

    <div kinesisMap [positions]="positions" [interpolation]="'smooth'" [playout]="'auto'"></div>
    

    Forwarded to the underlying Tracker only when set, so omitting the input keeps the classical real-time path. Use 'auto' for unknown feeds (Tracker self-calibrates from the gap history) or { pace, bufferMs, maxQueue } when you know your worst-case gap.

@kinesisjs/angularv0.3.22026-06-12
@kinesisjs/angularv0.3.12026-06-12
@kinesisjs/openlayersv0.2.52026-06-12
@kinesisjs/openlayersv0.2.42026-06-12
@kinesisjs/leafletv0.1.22026-06-12
@kinesisjs/route-awarev0.1.22026-06-12
@kinesisjs/leafletv0.1.12026-06-12
@kinesisjs/route-awarev0.1.12026-06-12
@kinesisjs/corev0.3.02026-05-28
  • Add opt-in Web Worker mode (worker: true or worker: { url }).

    The tick loop — interpolation, sanity checks, and the sweeper — can now run off the main thread inside a Web Worker, keeping the UI thread free for the actual map/DOM writes. The adapter stays on the main thread and is driven by messages the worker streams back, so existing adapters work unchanged.

    • worker: true spins the worker up from an inlined Blob (zero setup; adds ~2.4 KB gzip to the core bundle).
    • worker: { url } loads the bundled worker script from a URL you control, avoiding the inline payload.

    The public API is unchanged — new Tracker({ worker: true }) transparently returns a worker-backed tracker with the same surface. @kinesisjs/angular's [kinesisMap] directive exposes it via a new [worker] input.

    Caveats: a CustomInterpolator isn't supported in worker mode (functions can't cross the worker boundary; construction throws), updateOpacity-based fade animations degrade to snapping, and getStats() returns a snapshot refreshed every ~30 ticks.

@kinesisjs/angularv0.3.02026-05-28
  • Add opt-in Web Worker mode (worker: true or worker: { url }).

    The tick loop — interpolation, sanity checks, and the sweeper — can now run off the main thread inside a Web Worker, keeping the UI thread free for the actual map/DOM writes. The adapter stays on the main thread and is driven by messages the worker streams back, so existing adapters work unchanged.

    • worker: true spins the worker up from an inlined Blob (zero setup; adds ~2.4 KB gzip to the core bundle).
    • worker: { url } loads the bundled worker script from a URL you control, avoiding the inline payload.

    The public API is unchanged — new Tracker({ worker: true }) transparently returns a worker-backed tracker with the same surface. @kinesisjs/angular's [kinesisMap] directive exposes it via a new [worker] input.

    Caveats: a CustomInterpolator isn't supported in worker mode (functions can't cross the worker boundary; construction throws), updateOpacity-based fade animations degrade to snapping, and getStats() returns a snapshot refreshed every ~30 ticks.

  • Updated dependencies []:

@kinesisjs/openlayersv0.2.32026-05-28
  • Release pipeline restored — npm Trusted Publishing now verified end-to-end.

    No runtime changes. This patch only re-establishes the OIDC publish flow after the v0.1.2 / v0.2.0 / v0.2.1 release failures, by ensuring all three packages have valid Trusted Publisher rules on npmjs.com that match the release workflow.

  • Updated dependencies []:

  • Release pipeline restored — npm Trusted Publishing now verified end-to-end.

    No runtime changes. This patch only re-establishes the OIDC publish flow after the v0.1.2 / v0.2.0 / v0.2.1 release failures, by ensuring all three packages have valid Trusted Publisher rules on npmjs.com that match the release workflow.

  • Updated dependencies []:

  • Release pipeline restored — npm Trusted Publishing now verified end-to-end.

    No runtime changes. This patch only re-establishes the OIDC publish flow after the v0.1.2 / v0.2.0 / v0.2.1 release failures, by ensuring all three packages have valid Trusted Publisher rules on npmjs.com that match the release workflow.

  • Trail layer was invisible by default in v0.2.0 — the layer's zIndex: -1 default placed it BELOW the standard OSM tile layer (zIndex 0), which then overdrew the trail. Reported by the first downstream consumer that enabled trail: { enabled: true } without overriding zIndex.

    Fix:

    • Trail layer is now added to the map BEFORE the adapter's vehicle layer. OpenLayers' natural render order (later-added on top) puts trails behind vehicles without needing zIndex tricks.
    • TrailRenderOptions.zIndex default is now undefined (previously -1). The option remains available as an explicit override for existingLayer mode, where the user's own vehicle layer is already in the stack and trail-vs-vehicle order cannot be controlled by add sequence alone.

    Two new tests lock in the fix:

    • adds the trail layer BEFORE the vehicle layer (trail renders below)
    • honors explicit trail.zIndex when provided (existingLayer override)

    No API surface change; users who were already setting trail.zIndex explicitly keep their behavior. Users on the default config get visible trails.

  • KinesisMapDirective now exposes the warningOpacity adapter option as an optional @Input, completing the v0.2.0 gap-visualization story for directive users (previously reachable only via the kinesisTracker factory).

    <div kinesisMap [positions]="positions" [warningThreshold]="60000" [warningOpacity]="0.5"></div>
    

    Marker dims to 50% when a vehicle's idle exceeds warningThreshold; restores to 1.0 on the next ingest or sweeper-detected recovery. Omit the input to keep the v0.2.0 behavior (no opacity change on warning).

  • Updated dependencies []:

  • TrackAdapter gains an optional setVehicleState(id, state) hook. Tracker calls it whenever a vehicle transitions between lifecycle states (active ↔ warning), so adapters can render gap-visualization treatment — fading, badging, dashed trails — without having to subscribe to the event bus externally.

    The hook fires:

    • On warning (sweeper detects idle > warningThreshold)
    • On recovery to active (fresh ingest, or sweeper after slot revives)

    It does NOT fire for stale or completed — those are followed immediately by removeVehicle(id), and rendering a transient terminal state isn't useful.

    Backward compatible: the method is optional in the interface, and adapters that don't implement it (or instances on the existing v0.1.x API) keep working unchanged.

  • Gap visualization: OpenLayersAdapter now implements the new setVehicleState hook.

    Every state change always writes a vehicleState feature property (useful for external readers / popup labels). When OpenLayersAdapterOptions.warningOpacity is configured, the adapter additionally dims the marker on warning and restores opacity 1 on active:

    new OpenLayersAdapter(map, {
      style: vehicleStyle,
      warningOpacity: 0.5, // marker fades to 50% when warning threshold passes
    });
    

    stale and completed are handled by removeVehicle and produce no opacity work here. Without warningOpacity, only the property is set — no visual change (backward compatible default).

    Pairs naturally with the v0.2.0 trail rendering: the dimmed marker plus the still-rendered trail tell the user "we know the last position but haven't heard back" without removing the vehicle from the map.

  • Per-vehicle trail rendering — fading polyline behind each marker showing recent positions.

    Opt in via OpenLayersAdapter:

    new OpenLayersAdapter(map, {
      style: vehicleStyle,
      trail: { enabled: true, maxPoints: 60, intervalMs: 100, width: 3, opacity: 0.5 },
    });
    

    A separate VectorLayer (name: 'kinesis-trails', default zIndex: -1) is added when enabled — trails always render below vehicle markers regardless of the vehicle layer's own zIndex. Each vehicle gets a Feature<LineString> with id trail:<vehicleId>.

    Color resolution: explicit trail.colorTrailPoint.meta.color (string) → trail.defaultColor#3b82f6. Hex inputs (#rrggbb, #rgb) have the trail's opacity applied automatically as alpha; non-hex colors (named, rgb(), rgba()) are passed through unchanged so the caller controls alpha.

    Throttling: intervalMs (default 100 ms) caps how often a tick is appended to a trail. The Tracker runs at ~60 fps, so without throttling a 60-point buffer fills in one second. The default samples at ~10 Hz, giving a ~6-second visible trail.

    Memory: per-trail overhead ≈ 64 bytes + 16 bytes per coordinate; reflected in getMemoryEstimate(). Trail features are torn down with removeVehicle(id) and the trail layer is removed from the map in destroy().

    Backward compatible — adapter behaves identically to v0.1.x when trail is omitted or { enabled: false }.

  • Updated dependencies []:

  • KinesisMapDirective now exposes the new trail adapter option as an optional @Input.

    <div
      kinesisMap
      [positions]="positions"
      [trail]="{ enabled: true, maxPoints: 60, intervalMs: 100, color: '#3b82f6' }"
    ></div>
    

    Omit the input and the directive behaves identically to v0.1.2 (no trail layer created). See @kinesisjs/openlayers TrailRenderOptions for the full option surface.

  • Updated dependencies [,,]:

  • Lower AdaptiveInterpolator default minPeriodMs from 1000 to 500.

    At 1000 ms the default placed a typical 1 Hz GPS feed exactly on the boundary between the none and linear adaptive zones, and setInterval/interval(1000) jitter routinely produced sub-1000 ms periods. Each clipped tick fell into the none zone and teleported the marker — visible micro-skipping with the otherwise smooth renderLagMs buffer.

    The new 500 ms default keeps 1 Hz feeds firmly inside linear regardless of jitter. Sub-second feeds that explicitly want the none behavior can opt in:

    new Tracker({ adapter, interpolation: 'adaptive', adaptive: { minPeriodMs: 1000 } });
    

    No API change, only the default value.

  • KinesisMapDirective now exposes four advanced TrackerOptions as optional @Inputs. Previously these were only reachable via the lower-level kinesisTracker(...) factory.

    • [renderLagMs] — real-time interpolation buffer size (default 1000)
    • [adaptive] — adaptive zone thresholds object
    • [fadeAnimation] — duration / easing for the adaptive fade zone
    • [initialPositionBehavior]'show-immediately' | 'wait-for-second' | 'fade-in'

    Example:

    <div
      kinesisMap
      [positions]="positions"
      [interpolation]="'adaptive'"
      [renderLagMs]="800"
      [adaptive]="{ minPeriodMs: 200, fadeThresholdMs: 30000 }"
      [fadeAnimation]="{ duration: 400, easing: 'linear' }"
      [initialPositionBehavior]="'fade-in'"
    ></div>
    

    All four inputs are optional — omitting them keeps the tracker defaults.

  • Updated dependencies []:

  • Fix two critical issues discovered while building the first downstream Angular demo:

    @kinesisjs/core — real-time interpolation now actually runs. v0.1.0's Tracker.tick() always took the snap-to-current branch: at the moment a position was ingested, now == current.receivedAt, so elapsed = now − previous.receivedAt ≥ period immediately and stayed true, making interpolation unreachable outside fake-timer tests that rewind Date.now(). Added TrackerOptions.renderLagMs (default 1000 ms), the standard interpolation-buffer pattern from real-time networking: tick computes renderTime = now − renderLagMs and uses that for elapsed/ratio. With the default, a 1 Hz feed slides the marker smoothly from the previous to the current point over each second. Pass renderLagMs: 0 to restore the legacy snap-on-ingest behavior. Added two new tests covering both modes; the existing custom-interpolator tests no longer rely on vi.setSystemTime rewinding.

    @kinesisjs/angular — built with ng-packagr, finally importable by Angular AOT consumers. v0.1.0 was bundled with tsup, which preserved raw TS decorator output (__decorate([Directive({...})], cls)). Angular AOT consumers compile against Ivy partial-Ivy metadata (ɵdir, ɵfac, ɵngDeclareDirective, ɵngDeclareClassMetadata), which tsup does not emit — so imports: [KinesisMapDirective] in any consuming standalone component failed at AOT compile time with "Component imports must be standalone components, directives, pipes, or must be NgModules." Migrated build to ng-packagr (FESM2022 + partial-Ivy .d.ts); the package now compiles cleanly into Angular 17+ apps. No source-level API changes.

    @kinesisjs/openlayers — patch bump for monorepo cohesion only; no behavior change.

  • Fix two critical issues discovered while building the first downstream Angular demo:

    @kinesisjs/core — real-time interpolation now actually runs. v0.1.0's Tracker.tick() always took the snap-to-current branch: at the moment a position was ingested, now == current.receivedAt, so elapsed = now − previous.receivedAt ≥ period immediately and stayed true, making interpolation unreachable outside fake-timer tests that rewind Date.now(). Added TrackerOptions.renderLagMs (default 1000 ms), the standard interpolation-buffer pattern from real-time networking: tick computes renderTime = now − renderLagMs and uses that for elapsed/ratio. With the default, a 1 Hz feed slides the marker smoothly from the previous to the current point over each second. Pass renderLagMs: 0 to restore the legacy snap-on-ingest behavior. Added two new tests covering both modes; the existing custom-interpolator tests no longer rely on vi.setSystemTime rewinding.

    @kinesisjs/angular — built with ng-packagr, finally importable by Angular AOT consumers. v0.1.0 was bundled with tsup, which preserved raw TS decorator output (__decorate([Directive({...})], cls)). Angular AOT consumers compile against Ivy partial-Ivy metadata (ɵdir, ɵfac, ɵngDeclareDirective, ɵngDeclareClassMetadata), which tsup does not emit — so imports: [KinesisMapDirective] in any consuming standalone component failed at AOT compile time with "Component imports must be standalone components, directives, pipes, or must be NgModules." Migrated build to ng-packagr (FESM2022 + partial-Ivy .d.ts); the package now compiles cleanly into Angular 17+ apps. No source-level API changes.

    @kinesisjs/openlayers — patch bump for monorepo cohesion only; no behavior change.

  • Updated dependencies []:

  • Fix two critical issues discovered while building the first downstream Angular demo:

    @kinesisjs/core — real-time interpolation now actually runs. v0.1.0's Tracker.tick() always took the snap-to-current branch: at the moment a position was ingested, now == current.receivedAt, so elapsed = now − previous.receivedAt ≥ period immediately and stayed true, making interpolation unreachable outside fake-timer tests that rewind Date.now(). Added TrackerOptions.renderLagMs (default 1000 ms), the standard interpolation-buffer pattern from real-time networking: tick computes renderTime = now − renderLagMs and uses that for elapsed/ratio. With the default, a 1 Hz feed slides the marker smoothly from the previous to the current point over each second. Pass renderLagMs: 0 to restore the legacy snap-on-ingest behavior. Added two new tests covering both modes; the existing custom-interpolator tests no longer rely on vi.setSystemTime rewinding.

    @kinesisjs/angular — built with ng-packagr, finally importable by Angular AOT consumers. v0.1.0 was bundled with tsup, which preserved raw TS decorator output (__decorate([Directive({...})], cls)). Angular AOT consumers compile against Ivy partial-Ivy metadata (ɵdir, ɵfac, ɵngDeclareDirective, ɵngDeclareClassMetadata), which tsup does not emit — so imports: [KinesisMapDirective] in any consuming standalone component failed at AOT compile time with "Component imports must be standalone components, directives, pipes, or must be NgModules." Migrated build to ng-packagr (FESM2022 + partial-Ivy .d.ts); the package now compiles cleanly into Angular 17+ apps. No source-level API changes.

    @kinesisjs/openlayers — patch bump for monorepo cohesion only; no behavior change.

  • Updated dependencies []:

Initial public release.

  • Tracker orchestrator with validation, throttling, and configurable initial-position behaviour (show-immediately / wait-for-second / fade-in)
  • Interpolator modes: linear, cubic, geodesic, none
  • AdaptiveInterpolator — period-aware four-zone classifier
  • Sweeper — multi-state vehicle lifecycle (active / warning / stale / completed)
  • CustomInterpolator interface with sync/async support
  • Tick-loop sanity checks: anomalous-jump (haversine + speed) and sharp-turn (heading)
  • Event-based error handling — public methods never throw
  • Performance telemetry — tick history percentiles, dropped ticks, ingest rate, memory breakdown
  • Public utilities: haversineDistance, shortestArcDiff, linearLerp
  • Benchmarked: 1000 vehicles ≈ 0.15 ms per tick

Initial public release.

  • Full TrackAdapter implementation for OpenLayers
  • managedFeatureIds option to coexist safely with non-vehicle features inside a shared VectorLayer
  • updateOpacity capability for fade animations
  • getMemoryEstimate capability for accurate stats
  • createVehicleStyle helper with Icon / Circle modes, heading rotation, and speed colour bands
  • colorForSpeed exported standalone for custom style factories

Initial public release.

  • KinesisMapDirective — standalone directive for one-line setup
  • kinesisTracker factory for programmatic use in services and route resolvers
  • Automatic DestroyRef cleanup
  • Signal<Position[]> and Observable<Position[]> both supported as the input source
  • Peer dependency: Angular 17+
  • feat(leaflet): add the Leaflet map adapter

    New @kinesisjs/leaflet package — a TrackAdapter for Leaflet, on par with @kinesisjs/openlayers: per-vehicle L.Marker lifecycle, a built-in heading-aware rotatable marker (plus static/dynamic icon factories and the createVehicleStyle helper with speed-band colouring), managedFeatureIds, updateOpacity, setVehicleState + warningOpacity gap visualisation, and optional per-vehicle trail rendering. leaflet is a peer dependency (>=1.7).

  • #12 feat(route-aware): add the road-snapping CustomInterpolator package

    New @kinesisjs/route-aware — a CustomInterpolator for @kinesisjs/core that asks an OSRM server for the real road between two GPS points and walks that polyline at constant arc-length speed. Markers follow the actual street network instead of cutting straight lines across buildings.

    Highlights:

    • OSRMInterpolator drops into new Tracker({ interpolation: ri }) — no changes to the existing engine, no map adapter coupling.
    • The tick is never blocked: compute() is always synchronous. prepare() warms the cache in the background; cache misses fall back to a linear lerp this tick and snap to the road on the next.
    • LRU + coordinate-grid hashing → high cache hit rate (a 500-vehicle fleet on recurring routes typically generates tens of unique segment fetches).
    • Coalesces concurrent fetches for the same segment hash.
    • Detour guard (default 2.5× straight-line) rejects implausible routes — segment keeps using linear fallback rather than misleading the operator.
    • dispose() clears cache + in-flight set on tracker.destroy().

    Defaults point at the public router.project-osrm.org demo endpoint for evaluation; production fleets should self-host (see README + PRD §22).