@nolag/track

Vehicle and asset GPS tracking with geofencing and location history.

Overview

Track vehicles, delivery drivers, drones, or any moving asset in real time. @nolag/track broadcasts location updates to zone subscribers instantly. Geofence detection runs client-side using the haversine formula for circular boundaries and a ray-casting algorithm for polygon boundaries, so triggers fire without a server round-trip. Zones group assets by geographic area or fleet. Join multiple zones to observe overlapping regions. Your app owns one core NoLag client and injects it into NoLagTrack; the wrapper attaches its behaviour to that connection.

Key Features

  • Real-time GPS location broadcast to zone subscribers
  • In-memory location buffer per asset for client-side history
  • Client-side geofence detection for circle (haversine) and polygon (ray-casting)
  • Geofences narrow the server-side location filter to the grid cells they cover
  • Asset online/offline presence via lobby
  • Optional metadata attached to each location point
  • Automatic reconnect with zone and presence restoration

How It Works

NoLagTrack attaches to an injected @nolag/js-sdk client and manages a lobby that tracks which assets are online. Calling joinZone(name) returns a TrackingZone that subscribes to two topics: locations for GPS points and _geofence for geofence events published by your own services. Neither topic is logged server-side, which suits high-frequency GPS data. Location history is kept in memory on the client. Geofence evaluation happens locally on each locationUpdate, for your own points and for remote ones.

Geofences also shape what a zone receives. Every location is published with a filter naming its geo-grid cell (0.01 degree squares). When you add or remove a geofence, the zone recomputes the cells its geofences cover and calls setFilters on the locations subscription, so a zone with geofences only receives location updates from those cells. With no geofences, and no filters of your own, the subscription is a wildcard and receives every update in the zone. The app owns the socket lifecycle; the wrapper never opens or closes it.

TopicPurpose
locationsGPS location points with optional metadata, tagged with their geo-grid cell
_geofenceInbound GeofenceEvent messages from your own services. The SDK only listens here and re-emits them as geofenceTriggered; it never publishes to this topic

A fresh subscribe or an ordinary reconnect never replays history, and this SDK never triggers replay; see Replay for the one case where the broker replays messages.

Before you start. Create an app from the nolag-track-sdk blueprint. That seeds the rooms this SDK expects (fleet-zone) and the online lobby, and returns an app slug with a random suffix. That slug is the appName you pass to the wrapper. You can also do this in the portal: Apps, New App, pick the blueprint.

import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
const app = await api.apps.create({ name: "My App", blueprintId: "nolag-track-sdk" });
console.log(app.slug); // e.g. "my-app-a3f9": this is your appName

const actor = await api.actors.create({ name: "web-client", actorType: "user" });
console.log(actor.accessToken); // shown once; keep it

Installation

npm install @nolag/track @nolag/js-sdk

Shared connection. One core NoLag client can back several wrapper SDKs at once, for example asset tracking, a dashboard, and notify on a single socket, as long as each wrapper uses a distinct appName. Each wrapper attaches its handlers on construction and releases them with detach(), and never touches the socket itself. Your app owns connect() and disconnect().

Quick Start

import { NoLag } from '@nolag/js-sdk'
import { NoLagTrack } from '@nolag/track'

// The app owns one core client. In a browser, pass a token provider so the
// SDK can mint fresh short-lived client tokens from your backend.
const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)

// Inject the client into the track wrapper
const tracker = new NoLagTrack({
  client,
  appName: APP_SLUG, // the slug returned when you created the app
  assetId: 'drv_001',
  assetName: 'Van 12',
})

await client.connect()   // the app owns the connection
await tracker.ready()    // wrapper setup complete

// Join the seeded tracking zone (groups assets by geographic area or fleet)
const zone = tracker.joinZone('fleet-zone')

// Device side: report a location. Publishers never receive their own messages,
// so the wrapper stores the point locally and emits `locationUpdate` itself.
zone.sendLocation({ lat: 51.5074, lng: -0.1278, speed: 11.6 }, { driverId: 'drv_001' })

// Add a circular geofence (haversine distance check)
zone.addGeofence({
  id: 'depot-central',
  shape: 'circle',
  center: { lat: 51.5074, lng: -0.1278 },
  radiusMeters: 500,
  name: 'Central Depot',
})

// Add a polygon geofence (ray-casting algorithm)
zone.addGeofence({
  id: 'zone-east',
  shape: 'polygon',
  points: [
    { lat: 51.52, lng: -0.05 },
    { lat: 51.50, lng: -0.03 },
    { lat: 51.48, lng: -0.06 },
    { lat: 51.50, lng: -0.09 },
  ],
  name: 'East Zone',
})
// From here on this zone only receives location updates from the grid cells
// the two geofences cover. Remove every geofence to go back to receiving all.

// Controller side: listen for updates
zone.on('locationUpdate', ({ assetId, point, metadata, timestamp }) => {
  console.log(`Asset ${assetId} at ${point.lat}, ${point.lng}`, metadata, timestamp)
})

zone.on('geofenceTriggered', ({ assetId, geofenceId, type, point }) => {
  console.log(`Asset ${assetId} ${type} geofence ${geofenceId} at ${point.lat}, ${point.lng}`)
})

// Read the in-memory location history for one asset (synchronous)
const history = zone.getLocationHistory('drv_001')
console.log(`${history.length} location points in buffer`)

// Teardown: the wrapper releases its handlers; the app closes the socket.
tracker.detach()
client.disconnect()

API Reference

NoLagTrack

Constructor Options

OptionTypeDescription
clientNoLagSocketRequired. The injected core NoLag client the app owns and connects.
appNamestringThe app slug used as the topic prefix. Pass the suffixed slug returned when you created the app; the default 'track' will not match a hosted app.
assetIdstringStable identifier for this asset (auto-generated if omitted).
assetNamestringOptional human-readable name for this asset.
metadataRecord<string, unknown>Optional custom data attached to asset presence.
zoneNamesstring[]Tracking zones to auto-join once the wrapper is ready.
zonesGeofence[]Client-side geofences to register on every joined zone.
maxLocationHistorynumberMax location history entries per asset (default 500).
debugbooleanEnable wrapper debug logging (default false).
MethodReturnsDescription
ready()Promise<void>Resolves once wrapper setup completed. Join methods throw before this resolves.
detach()voidRelease this wrapper's handlers and topics; terminal, never closes the socket.
joinZone(name, opts?)TrackingZoneSubscribe to a tracking zone and return it. Synchronous; returns the existing instance if already joined. opts.filters adds location filter values of your own, unioned with the geofence cells.
leaveZone(name)voidUnsubscribe from a zone and release its resources.
getZones()TrackingZone[]All currently joined zones.
getOnlineAssets()TrackedAsset[]Assets currently present in the online lobby.

NoLagTrack Events

EventPayloadDescription
connectednoneWrapper setup completed on a live connection.
disconnectedreason: stringConnection closed.
reconnectingnoneThe core client is attempting to reconnect.
reconnectednoneConnection restored after a drop; zone membership and presence are restored automatically.
errorerror: ErrorA transport or protocol error occurred.
assetOnlineasset: TrackedAssetAn asset joined the lobby.
assetOfflineasset: TrackedAssetAn asset left the lobby.

TrackingZone

MethodReturnsDescription
sendLocation(point, metadata?, opts?)LocationUpdateBroadcast a GeoPoint with optional metadata and return the update that was sent. Tagged with its grid cell unless opts.filter replaces it, which opts the update out of geofence routing.
getLocationHistory(assetId?)LocationUpdate[]Read location points from the in-memory buffer (up to maxLocationHistory per asset). Omit assetId to get all assets.
addGeofence(geofence)voidRegister a circle or polygon geofence, evaluated on every location update. Recomputes the grid-cell filter on locations.
removeGeofence(id)voidDeregister a geofence by its ID and recompute the filter.
getGeofences()Geofence[]All geofences registered on this zone.
getAssets()TrackedAsset[]Remote assets currently present in this zone.
getAsset(assetId)TrackedAsset | undefinedOne remote asset by ID.
setFilters(values)voidReplace your own location filter values. They are unioned with the geofence cells, never replace them; an empty array leaves only the geofence cells.
addFilters(values) / removeFilters(values)voidAdd or remove your own filter values without touching the geofence cells.

TrackingZone Events

EventPayloadDescription
locationUpdateLocationUpdateA location point was received from another asset, or sent by this one.
assetJoinedasset: TrackedAssetAn asset joined this zone.
assetLeftasset: TrackedAssetAn asset left this zone.
geofenceTriggeredGeofenceEventAn asset crossed a geofence boundary. Evaluated client-side on each location update, or received on _geofence.

Types

TypeShape
GeoPoint{ lat, lng, altitude?, accuracy?, heading?, speed? } (degrees, metres, metres per second)
LocationUpdate{ id, assetId, point: GeoPoint, metadata?, timestamp, isReplay }
Geofence{ id, shape: 'circle', center: GeoPoint, radiusMeters, name?, metadata? } or { id, shape: 'polygon', points: GeoPoint[], name?, metadata? }
GeofenceEvent{ geofenceId, assetId, type: 'enter' | 'exit', point: GeoPoint, timestamp }
TrackedAsset{ assetId, actorTokenId, assetName?, lastLocation?, metadata?, joinedAt, isLocal }