@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.
| Topic | Purpose |
|---|---|
locations | GPS location points with optional metadata, tagged with their geo-grid cell |
_geofence | Inbound 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 itInstallation
npm install @nolag/track @nolag/js-sdkShared 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
| Option | Type | Description |
|---|---|---|
client | NoLagSocket | Required. The injected core NoLag client the app owns and connects. |
appName | string | The 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. |
assetId | string | Stable identifier for this asset (auto-generated if omitted). |
assetName | string | Optional human-readable name for this asset. |
metadata | Record<string, unknown> | Optional custom data attached to asset presence. |
zoneNames | string[] | Tracking zones to auto-join once the wrapper is ready. |
zones | Geofence[] | Client-side geofences to register on every joined zone. |
maxLocationHistory | number | Max location history entries per asset (default 500). |
debug | boolean | Enable wrapper debug logging (default false). |
| Method | Returns | Description |
|---|---|---|
ready() | Promise<void> | Resolves once wrapper setup completed. Join methods throw before this resolves. |
detach() | void | Release this wrapper's handlers and topics; terminal, never closes the socket. |
joinZone(name, opts?) | TrackingZone | Subscribe 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) | void | Unsubscribe from a zone and release its resources. |
getZones() | TrackingZone[] | All currently joined zones. |
getOnlineAssets() | TrackedAsset[] | Assets currently present in the online lobby. |
NoLagTrack Events
| Event | Payload | Description |
|---|---|---|
connected | none | Wrapper setup completed on a live connection. |
disconnected | reason: string | Connection closed. |
reconnecting | none | The core client is attempting to reconnect. |
reconnected | none | Connection restored after a drop; zone membership and presence are restored automatically. |
error | error: Error | A transport or protocol error occurred. |
assetOnline | asset: TrackedAsset | An asset joined the lobby. |
assetOffline | asset: TrackedAsset | An asset left the lobby. |
TrackingZone
| Method | Returns | Description |
|---|---|---|
sendLocation(point, metadata?, opts?) | LocationUpdate | Broadcast 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) | void | Register a circle or polygon geofence, evaluated on every location update. Recomputes the grid-cell filter on locations. |
removeGeofence(id) | void | Deregister 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 | undefined | One remote asset by ID. |
setFilters(values) | void | Replace 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) | void | Add or remove your own filter values without touching the geofence cells. |
TrackingZone Events
| Event | Payload | Description |
|---|---|---|
locationUpdate | LocationUpdate | A location point was received from another asset, or sent by this one. |
assetJoined | asset: TrackedAsset | An asset joined this zone. |
assetLeft | asset: TrackedAsset | An asset left this zone. |
geofenceTriggered | GeofenceEvent | An asset crossed a geofence boundary. Evaluated client-side on each location update, or received on _geofence. |
Types
| Type | Shape |
|---|---|
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 } |