@aeroswarm/client 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -144,12 +144,13 @@ var AeroWsClient = class {
144
144
  this._requestSnapshot();
145
145
  }
146
146
  this.lastSeq = msg.seq;
147
+ const meta = { serverTime: msg.serverTime, seq: msg.seq };
147
148
  switch (msg.type) {
148
149
  case "snapshot":
149
- this.callbacks.onSnapshot?.(msg.payload.tracks);
150
+ this.callbacks.onSnapshot?.(msg.payload.tracks, meta);
150
151
  break;
151
152
  case "track.updated":
152
- this.callbacks.onTrackUpdate?.(msg.payload.track);
153
+ this.callbacks.onTrackUpdate?.(msg.payload.track, meta);
153
154
  break;
154
155
  case "track.removed":
155
156
  this.callbacks.onTrackRemoved?.(msg.payload);
@@ -317,6 +318,85 @@ function createAeroTrpcClient(endpoint, token) {
317
318
  });
318
319
  }
319
320
 
321
+ // src/sdk/api/response.ts
322
+ var ENVELOPE_HEADER = "x-aero-envelope";
323
+ var STATUS_CODES = [
324
+ [401, "UNAUTHORIZED"],
325
+ [403, "FORBIDDEN"],
326
+ [404, "NOT_FOUND"],
327
+ [400, "BAD_REQUEST"],
328
+ [409, "CONFLICT"],
329
+ [429, "RATE_LIMITED"],
330
+ [503, "UNAVAILABLE"],
331
+ [500, "INTERNAL"]
332
+ ];
333
+ function codeForStatus(status) {
334
+ const hit = STATUS_CODES.find(([s]) => s === status);
335
+ if (hit) return hit[1];
336
+ return status >= 500 ? "INTERNAL" : "BAD_REQUEST";
337
+ }
338
+ var AeroApiError = class extends Error {
339
+ status;
340
+ code;
341
+ /** Retrying later may succeed (rate limit, server busy or failing). */
342
+ recoverable;
343
+ details;
344
+ constructor(init) {
345
+ super(init.message);
346
+ this.name = "AeroApiError";
347
+ this.status = init.status;
348
+ this.code = init.code;
349
+ this.recoverable = init.recoverable;
350
+ this.details = init.details;
351
+ }
352
+ };
353
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
354
+ function isEnvelope(body) {
355
+ return isObject(body) && body.ok === true && "data" in body;
356
+ }
357
+ function isFailure(body) {
358
+ return isObject(body) && body.ok === false;
359
+ }
360
+ function errorFrom(res, body, text, label) {
361
+ const prefix = label ? `${label} \u2192 ${res.status}` : `HTTP ${res.status}`;
362
+ const failure = isFailure(body) ? body : void 0;
363
+ const e = failure?.error;
364
+ if (isObject(e)) {
365
+ const code = typeof e.code === "string" ? e.code : codeForStatus(res.status);
366
+ const message2 = typeof e.message === "string" ? e.message : res.statusText || prefix;
367
+ return new AeroApiError({
368
+ status: res.status,
369
+ code,
370
+ message: `${prefix}: ${message2}`,
371
+ recoverable: typeof e.recoverable === "boolean" ? e.recoverable : res.status >= 500,
372
+ details: e.details
373
+ });
374
+ }
375
+ const message = typeof e === "string" ? e : text.slice(0, 200) || res.statusText;
376
+ return new AeroApiError({
377
+ status: res.status,
378
+ code: codeForStatus(res.status),
379
+ message: message ? `${prefix}: ${message}` : prefix,
380
+ recoverable: res.status >= 500 || res.status === 429,
381
+ details: failure?.detail
382
+ });
383
+ }
384
+ async function readApiBody(res, label) {
385
+ const text = await res.text();
386
+ let body;
387
+ try {
388
+ body = text ? JSON.parse(text) : void 0;
389
+ } catch {
390
+ body = void 0;
391
+ }
392
+ if (!res.ok || isFailure(body)) throw errorFrom(res, body, text, label);
393
+ return body;
394
+ }
395
+ async function parseApiResponse(res, label) {
396
+ const body = await readApiBody(res, label);
397
+ return isEnvelope(body) ? body.data : body;
398
+ }
399
+
320
400
  // src/sdk/api/rest.ts
321
401
  var AeroRestClient = class {
322
402
  constructor(endpoint, token) {
@@ -334,8 +414,7 @@ var AeroRestClient = class {
334
414
  },
335
415
  body: body ? JSON.stringify(body) : void 0
336
416
  });
337
- if (!res.ok) throw new Error(`${method} ${path} \u2192 ${res.status}`);
338
- return res.json();
417
+ return await readApiBody(res, `${method} ${path}`);
339
418
  }
340
419
  createSource(body) {
341
420
  return this.req("POST", "/v1/sources", body);
@@ -557,9 +636,10 @@ var AeroSwarm = class _AeroSwarm {
557
636
  async federatedFetch(base, path, query) {
558
637
  const url = new URL(`${base}${path}`);
559
638
  for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
560
- const res = await fetch(url, { headers: { Authorization: `Bearer ${this.opts.token}` } });
561
- if (!res.ok) throw new Error(`Federated request failed: ${res.status} ${res.statusText} (${url.pathname})`);
562
- return res.json();
639
+ const res = await fetch(url, {
640
+ headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
641
+ });
642
+ return parseApiResponse(res, `Federated request ${url.pathname}`);
563
643
  }
564
644
  tracks = {
565
645
  subscribe: (opts, callbacks) => {
@@ -571,15 +651,15 @@ var AeroSwarm = class _AeroSwarm {
571
651
  this._status = s;
572
652
  callbacks.onStatusChange?.(s);
573
653
  },
574
- onSnapshot: (tracks) => {
654
+ onSnapshot: (tracks, meta) => {
575
655
  const labeled = this.cache.applySnapshot(tracks);
576
656
  this._lastUpdateAt = Date.now();
577
- callbacks.onSnapshot?.(labeled);
657
+ callbacks.onSnapshot?.(labeled, meta);
578
658
  },
579
- onTrackUpdate: (track) => {
659
+ onTrackUpdate: (track, meta) => {
580
660
  const labeled = this.cache.applyUpdate(track);
581
661
  this._lastUpdateAt = Date.now();
582
- callbacks.onTrackUpdate?.(labeled);
662
+ callbacks.onTrackUpdate?.(labeled, meta);
583
663
  },
584
664
  onTrackRemoved: (ev) => {
585
665
  this.cache.applyRemoved(ev);
@@ -604,7 +684,7 @@ var AeroSwarm = class _AeroSwarm {
604
684
  reconnect: () => this.wsClient.reconnect(),
605
685
  stream: (opts) => {
606
686
  const queue = [];
607
- let resolve = null;
687
+ let resolve2 = null;
608
688
  let done = false;
609
689
  this.wsClient.connect({ encoding: "json", ...opts }, {
610
690
  onSnapshot: (tracks) => {
@@ -612,9 +692,9 @@ var AeroSwarm = class _AeroSwarm {
612
692
  },
613
693
  onTrackUpdate: (raw) => {
614
694
  const t = this.cache.applyUpdate(raw);
615
- if (resolve) {
616
- resolve({ value: t, done: false });
617
- resolve = null;
695
+ if (resolve2) {
696
+ resolve2({ value: t, done: false });
697
+ resolve2 = null;
618
698
  } else queue.push(t);
619
699
  },
620
700
  onStatusChange: (s) => {
@@ -627,7 +707,7 @@ var AeroSwarm = class _AeroSwarm {
627
707
  if (queue.length) return Promise.resolve({ value: queue.shift(), done: false });
628
708
  if (done) return Promise.resolve({ value: void 0, done: true });
629
709
  return new Promise((r) => {
630
- resolve = r;
710
+ resolve2 = r;
631
711
  });
632
712
  },
633
713
  return: async () => {
@@ -820,8 +900,331 @@ function AeroSwarmProvider({
820
900
  return /* @__PURE__ */ jsx(QueryClientProvider, { client: qc, children: /* @__PURE__ */ jsx(AeroSwarmContext.Provider, { value: { client }, children }) });
821
901
  }
822
902
 
903
+ // src/react/hooks/use-track-motion.ts
904
+ import { useEffect, useMemo as useMemo2, useRef } from "react";
905
+
906
+ // src/shared/motion/index.ts
907
+ var KT_TO_MS = 0.514444;
908
+ var M_PER_DEG = 111320;
909
+ var MAX_PLAUSIBLE_KT = 1500;
910
+ var OFFSET_WINDOW = 20;
911
+ var ALT_EPSILON_FT = 10;
912
+ var HEADING_EPSILON_DEG = 0.5;
913
+ var SURFACE_ALT_FT = 150;
914
+ var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
915
+ var smoothstep = (x) => {
916
+ const c = clamp01(x);
917
+ return c * c * (3 - 2 * c);
918
+ };
919
+ var wrapDeg = (d) => ((d + 180) % 360 + 360) % 360 - 180;
920
+ var norm360 = (d) => (d % 360 + 360) % 360;
921
+ var cosLat = (lat) => Math.max(0.01, Math.cos(lat * Math.PI / 180));
922
+ function validHeading(h) {
923
+ return h != null && Number.isFinite(h) && h >= 0 && h <= 360 ? h % 360 : null;
924
+ }
925
+ function move(lat, lon, bearingDeg, distM) {
926
+ const rad = bearingDeg * Math.PI / 180;
927
+ const dLat = distM * Math.cos(rad) / M_PER_DEG;
928
+ const dLon = distM * Math.sin(rad) / (M_PER_DEG * cosLat(lat + dLat / 2));
929
+ return { lat: lat + dLat, lon: lon + dLon };
930
+ }
931
+ function distanceM(aLat, aLon, bLat, bLon) {
932
+ const dy = (bLat - aLat) * M_PER_DEG;
933
+ const dx = (bLon - aLon) * M_PER_DEG * cosLat((aLat + bLat) / 2);
934
+ return Math.hypot(dx, dy);
935
+ }
936
+ var headingChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(wrapDeg(a - b)) > HEADING_EPSILON_DEG;
937
+ var altitudeChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(a - b) >= ALT_EPSILON_FT;
938
+ var defaultIsSurface = (t) => t.targetType === "vehicle" || t.altitudeFt != null && t.altitudeFt <= SURFACE_ALT_FT;
939
+ function resolve(o) {
940
+ return {
941
+ blendMs: o.blendMs ?? 1500,
942
+ headingSmoothMs: o.headingSmoothMs ?? 800,
943
+ maxExtrapolateMs: o.maxExtrapolateMs ?? { air: 2e4, surface: 8e3 },
944
+ minMovingKt: o.minMovingKt ?? 5,
945
+ snapDistanceM: o.snapDistanceM ?? 5e3,
946
+ frameHz: o.frameHz ?? 30,
947
+ epsilonM: o.epsilonM ?? 0.5,
948
+ extrapolate: o.extrapolate ?? true,
949
+ isSurface: o.isSurface ?? defaultIsSurface,
950
+ clock: o.clock ?? Date.now,
951
+ scheduler: o.scheduler
952
+ };
953
+ }
954
+ function defaultScheduler() {
955
+ const g = globalThis;
956
+ if (typeof g.requestAnimationFrame === "function" && typeof g.cancelAnimationFrame === "function") {
957
+ return {
958
+ request: (cb) => g.requestAnimationFrame(cb),
959
+ cancel: (h) => g.cancelAnimationFrame(h)
960
+ };
961
+ }
962
+ return {
963
+ request: (cb) => setTimeout(() => cb(Date.now()), 16),
964
+ cancel: (h) => clearTimeout(h)
965
+ };
966
+ }
967
+ var Motion = class {
968
+ o;
969
+ entries = /* @__PURE__ */ new Map();
970
+ /** Tracks whose picture can still change. Everything else costs nothing per frame. */
971
+ active = /* @__PURE__ */ new Set();
972
+ listeners = /* @__PURE__ */ new Set();
973
+ offsets = [];
974
+ offset = 0;
975
+ handle = null;
976
+ lastFrameAt = Number.NEGATIVE_INFINITY;
977
+ constructor(options) {
978
+ this.o = resolve(options);
979
+ }
980
+ get size() {
981
+ return this.entries.size;
982
+ }
983
+ offsetMs() {
984
+ return this.offset;
985
+ }
986
+ // ── time ────────────────────────────────────────────────────────────────
987
+ /** Browser clock + learned offset = the server's clock. */
988
+ serverNow(at) {
989
+ return (at ?? this.o.clock()) + this.offset;
990
+ }
991
+ /**
992
+ * `serverTime - browserNow` is the true offset MINUS the network delay, so the
993
+ * largest recent value is the one with the least delay, i.e. the best estimate.
994
+ */
995
+ observe(serverTime) {
996
+ if (!Number.isFinite(serverTime)) return;
997
+ this.offsets.push(serverTime - this.o.clock());
998
+ if (this.offsets.length > OFFSET_WINDOW) this.offsets.shift();
999
+ this.offset = Math.max(...this.offsets);
1000
+ }
1001
+ cap(e) {
1002
+ const m = this.o.maxExtrapolateMs;
1003
+ return e.surface ? m.surface : m.air;
1004
+ }
1005
+ // ── model ───────────────────────────────────────────────────────────────
1006
+ posAt(e, s) {
1007
+ const cap = this.cap(e);
1008
+ let elapsed = Math.min(Math.max(0, s - e.fixAt), cap);
1009
+ if (e.frozen !== null) elapsed = Math.min(elapsed, e.frozen);
1010
+ if (!this.o.extrapolate || e.course === null || e.speedMs <= 0) {
1011
+ return { lat: e.fixLat, lon: e.fixLon, extrapolating: false };
1012
+ }
1013
+ const p = move(e.fixLat, e.fixLon, e.course, e.speedMs * elapsed / 1e3);
1014
+ return { ...p, extrapolating: e.frozen === null && elapsed < cap };
1015
+ }
1016
+ sampleAt(e, s) {
1017
+ const p = this.posAt(e, s);
1018
+ const rest = 1 - smoothstep((s - e.blendStart) / this.o.blendMs);
1019
+ let headingDeg = null;
1020
+ if (e.hdgTo !== null) {
1021
+ const from = e.hdgFrom ?? e.hdgTo;
1022
+ const left = 1 - smoothstep((s - e.hdgStart) / this.o.headingSmoothMs);
1023
+ headingDeg = norm360(e.hdgTo + wrapDeg(from - e.hdgTo) * left);
1024
+ }
1025
+ let altitudeFt = null;
1026
+ if (e.altTo !== null) {
1027
+ const k = smoothstep((s - e.altStart) / this.o.blendMs);
1028
+ altitudeFt = e.altFrom === null ? e.altTo : e.altFrom + (e.altTo - e.altFrom) * k;
1029
+ }
1030
+ return {
1031
+ lat: p.lat + e.errLat * rest,
1032
+ lon: p.lon + e.errLon * rest,
1033
+ headingDeg,
1034
+ altitudeFt,
1035
+ moving: e.speedMs > 0,
1036
+ extrapolating: p.extrapolating
1037
+ };
1038
+ }
1039
+ // ── input ───────────────────────────────────────────────────────────────
1040
+ update(t, meta) {
1041
+ if (meta?.serverTime !== void 0) this.observe(meta.serverTime);
1042
+ if (t.lat == null || t.lon == null || !Number.isFinite(t.lat) || !Number.isFinite(t.lon)) {
1043
+ this.remove(t.trackId);
1044
+ return;
1045
+ }
1046
+ const s = this.serverNow();
1047
+ const e = this.entries.get(t.trackId);
1048
+ if (!e) {
1049
+ this.create(t, s);
1050
+ return;
1051
+ }
1052
+ this.refresh(e, t, s);
1053
+ }
1054
+ moving(t, course) {
1055
+ const kt = t.groundSpeedKt;
1056
+ return course !== null && kt != null && Number.isFinite(kt) && kt >= this.o.minMovingKt && kt <= MAX_PLAUSIBLE_KT;
1057
+ }
1058
+ create(t, s) {
1059
+ const course = validHeading(t.headingDeg);
1060
+ const moving = this.moving(t, course);
1061
+ const e = {
1062
+ id: t.trackId,
1063
+ fixLat: t.lat,
1064
+ fixLon: t.lon,
1065
+ fixAt: t.lastSeenAt,
1066
+ speedMs: moving ? t.groundSpeedKt * KT_TO_MS : 0,
1067
+ course,
1068
+ surface: this.o.isSurface(t),
1069
+ frozen: null,
1070
+ errLat: 0,
1071
+ errLon: 0,
1072
+ blendStart: s,
1073
+ hdgFrom: course,
1074
+ hdgTo: course,
1075
+ hdgStart: s,
1076
+ altFrom: null,
1077
+ altTo: t.altitudeFt,
1078
+ altStart: s,
1079
+ settleAt: 0,
1080
+ emitted: null
1081
+ };
1082
+ if (t.status === "lost") e.frozen = this.frozenAt(e, s);
1083
+ this.entries.set(e.id, e);
1084
+ this.settle(e);
1085
+ this.active.add(e.id);
1086
+ this.ensureLoop();
1087
+ }
1088
+ frozenAt(e, s) {
1089
+ return Math.min(Math.max(0, s - e.fixAt), this.cap(e));
1090
+ }
1091
+ refresh(e, t, s) {
1092
+ e.surface = this.o.isSurface(t);
1093
+ if (t.status === "lost") e.frozen ??= this.frozenAt(e, s);
1094
+ else e.frozen = null;
1095
+ const lat = t.lat;
1096
+ const lon = t.lon;
1097
+ const older = t.lastSeenAt < e.fixAt;
1098
+ const same = t.lastSeenAt === e.fixAt && lat === e.fixLat && lon === e.fixLon;
1099
+ if (older || same) {
1100
+ this.settle(e);
1101
+ this.touch(e);
1102
+ return;
1103
+ }
1104
+ const shown = this.sampleAt(e, s);
1105
+ const course = validHeading(t.headingDeg) ?? e.course;
1106
+ const moving = this.moving(t, course);
1107
+ e.fixLat = lat;
1108
+ e.fixLon = lon;
1109
+ e.fixAt = t.lastSeenAt;
1110
+ e.speedMs = moving ? t.groundSpeedKt * KT_TO_MS : 0;
1111
+ e.course = course;
1112
+ const predicted = this.posAt(e, s);
1113
+ const jump = distanceM(shown.lat, shown.lon, predicted.lat, predicted.lon);
1114
+ const snap = jump > this.o.snapDistanceM;
1115
+ e.errLat = snap ? 0 : shown.lat - predicted.lat;
1116
+ e.errLon = snap ? 0 : shown.lon - predicted.lon;
1117
+ e.blendStart = s;
1118
+ e.hdgFrom = snap ? course : shown.headingDeg ?? course;
1119
+ e.hdgTo = course;
1120
+ e.hdgStart = s;
1121
+ e.altFrom = snap ? t.altitudeFt : shown.altitudeFt;
1122
+ e.altTo = t.altitudeFt;
1123
+ e.altStart = s;
1124
+ this.settle(e);
1125
+ this.touch(e);
1126
+ }
1127
+ /** The server time after which nothing about this track changes on its own. */
1128
+ settle(e) {
1129
+ const ends = [e.blendStart + this.o.blendMs, e.hdgStart + this.o.headingSmoothMs, e.altStart + this.o.blendMs];
1130
+ if (this.o.extrapolate && e.speedMs > 0 && e.frozen === null) ends.push(e.fixAt + this.cap(e));
1131
+ e.settleAt = Math.max(...ends);
1132
+ }
1133
+ touch(e) {
1134
+ this.active.add(e.id);
1135
+ this.ensureLoop();
1136
+ }
1137
+ remove(trackId) {
1138
+ this.entries.delete(trackId);
1139
+ this.active.delete(trackId);
1140
+ }
1141
+ clear() {
1142
+ this.entries.clear();
1143
+ this.active.clear();
1144
+ }
1145
+ // ── output ──────────────────────────────────────────────────────────────
1146
+ sample(trackId, at) {
1147
+ const e = this.entries.get(trackId);
1148
+ return e ? this.sampleAt(e, this.serverNow(at)) : void 0;
1149
+ }
1150
+ differs(a, b) {
1151
+ if (a === null || a.moving !== b.moving || a.extrapolating !== b.extrapolating) return true;
1152
+ if (distanceM(a.lat, a.lon, b.lat, b.lon) > this.o.epsilonM) return true;
1153
+ return headingChanged(a.headingDeg, b.headingDeg) || altitudeChanged(a.altitudeFt, b.altitudeFt);
1154
+ }
1155
+ tick(at) {
1156
+ const s = this.serverNow(at);
1157
+ const changed = /* @__PURE__ */ new Map();
1158
+ for (const id of [...this.active]) {
1159
+ const e = this.entries.get(id);
1160
+ if (!e) {
1161
+ this.active.delete(id);
1162
+ continue;
1163
+ }
1164
+ const sample = this.sampleAt(e, s);
1165
+ if (this.differs(e.emitted, sample)) {
1166
+ e.emitted = sample;
1167
+ changed.set(id, sample);
1168
+ }
1169
+ if (s >= e.settleAt) this.active.delete(id);
1170
+ }
1171
+ return changed;
1172
+ }
1173
+ // ── frame loop ──────────────────────────────────────────────────────────
1174
+ start(onFrame) {
1175
+ this.listeners.add(onFrame);
1176
+ this.ensureLoop();
1177
+ return () => {
1178
+ this.listeners.delete(onFrame);
1179
+ if (this.listeners.size === 0) this.cancelLoop();
1180
+ };
1181
+ }
1182
+ ensureLoop() {
1183
+ if (this.handle !== null || this.listeners.size === 0 || this.active.size === 0) return;
1184
+ const scheduler = this.o.scheduler ?? (this.o.scheduler = defaultScheduler());
1185
+ this.handle = scheduler.request((ts) => this.frame(ts));
1186
+ }
1187
+ cancelLoop() {
1188
+ if (this.handle === null) return;
1189
+ (this.o.scheduler ?? defaultScheduler()).cancel(this.handle);
1190
+ this.handle = null;
1191
+ }
1192
+ frame(ts) {
1193
+ this.handle = null;
1194
+ if (this.listeners.size === 0) return;
1195
+ if (ts - this.lastFrameAt >= 1e3 / this.o.frameHz - 1) {
1196
+ this.lastFrameAt = ts;
1197
+ const changed = this.tick();
1198
+ if (changed.size > 0) {
1199
+ const frame = { at: this.serverNow(), changed };
1200
+ for (const fn of [...this.listeners]) fn(frame);
1201
+ }
1202
+ }
1203
+ this.ensureLoop();
1204
+ }
1205
+ };
1206
+ function createTrackMotion(options = {}) {
1207
+ return new Motion(options);
1208
+ }
1209
+
1210
+ // src/react/hooks/use-track-motion.ts
1211
+ function useTrackMotion(options) {
1212
+ const { onFrame, ...motionOptions } = options;
1213
+ const onFrameRef = useRef(onFrame);
1214
+ onFrameRef.current = onFrame;
1215
+ const motion = useMemo2(() => createTrackMotion(motionOptions), []);
1216
+ useEffect(() => {
1217
+ const stop = motion.start((frame) => onFrameRef.current(frame));
1218
+ return () => {
1219
+ stop();
1220
+ motion.clear();
1221
+ };
1222
+ }, [motion]);
1223
+ return motion;
1224
+ }
1225
+
823
1226
  // src/react/hooks/use-track-history.ts
824
- import { useCallback, useEffect, useRef } from "react";
1227
+ import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
825
1228
  var DR_MIN_SPEED_KT = 10;
826
1229
  var MAX_SPEED_KT = 1200;
827
1230
  var GAP_RESET_MS = 3e4;
@@ -841,22 +1244,22 @@ function useTrackHistory(options) {
841
1244
  maxJumpDeg = 5,
842
1245
  onFrame
843
1246
  } = options;
844
- const currentPositions = useRef(/* @__PURE__ */ new Map());
845
- const lastActualPos = useRef(/* @__PURE__ */ new Map());
846
- const history = useRef(/* @__PURE__ */ new Map());
847
- const lastSeen = useRef(/* @__PURE__ */ new Map());
848
- const lastUpdPerf = useRef(/* @__PURE__ */ new Map());
849
- const interp = useRef(/* @__PURE__ */ new Map());
850
- const velocity = useRef(/* @__PURE__ */ new Map());
851
- const drState = useRef(/* @__PURE__ */ new Map());
852
- const rafRef = useRef(null);
853
- const onFrameRef = useRef(onFrame);
1247
+ const currentPositions = useRef2(/* @__PURE__ */ new Map());
1248
+ const lastActualPos = useRef2(/* @__PURE__ */ new Map());
1249
+ const history = useRef2(/* @__PURE__ */ new Map());
1250
+ const lastSeen = useRef2(/* @__PURE__ */ new Map());
1251
+ const lastUpdPerf = useRef2(/* @__PURE__ */ new Map());
1252
+ const interp = useRef2(/* @__PURE__ */ new Map());
1253
+ const velocity = useRef2(/* @__PURE__ */ new Map());
1254
+ const drState = useRef2(/* @__PURE__ */ new Map());
1255
+ const rafRef = useRef2(null);
1256
+ const onFrameRef = useRef2(onFrame);
854
1257
  onFrameRef.current = onFrame;
855
- const maxHistoryPointsRef = useRef(maxHistoryPoints);
1258
+ const maxHistoryPointsRef = useRef2(maxHistoryPoints);
856
1259
  maxHistoryPointsRef.current = maxHistoryPoints;
857
- const minDeltaDegRef = useRef(minDeltaDeg);
1260
+ const minDeltaDegRef = useRef2(minDeltaDeg);
858
1261
  minDeltaDegRef.current = minDeltaDeg;
859
- const maxJumpDegRef = useRef(maxJumpDeg);
1262
+ const maxJumpDegRef = useRef2(maxJumpDeg);
860
1263
  maxJumpDegRef.current = maxJumpDeg;
861
1264
  function emitFrame() {
862
1265
  onFrameRef.current({
@@ -1019,7 +1422,7 @@ function useTrackHistory(options) {
1019
1422
  drState.current.delete(trackId);
1020
1423
  emitFrame();
1021
1424
  }, []);
1022
- useEffect(() => {
1425
+ useEffect2(() => {
1023
1426
  return () => {
1024
1427
  if (rafRef.current !== null) {
1025
1428
  cancelAnimationFrame(rafRef.current);
@@ -1034,7 +1437,7 @@ function useTrackHistory(options) {
1034
1437
  }
1035
1438
 
1036
1439
  // src/react/hooks/index.ts
1037
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
1440
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3 } from "react";
1038
1441
  import { useQuery } from "@tanstack/react-query";
1039
1442
  import { useShallow } from "zustand/react/shallow";
1040
1443
 
@@ -1134,7 +1537,7 @@ function useAeroTracks(opts = {}) {
1134
1537
  const isDegraded = useTrackStore(selectIsDegraded);
1135
1538
  const lastUpdateAt = useTrackStore(selectLastUpdateAt);
1136
1539
  const error = useTrackStore(selectError);
1137
- const unsubRef = useRef2(null);
1540
+ const unsubRef = useRef3(null);
1138
1541
  const { isLoading } = useQuery({
1139
1542
  queryKey: ["aero-snapshot", opts.bbox?.join(","), opts.source, opts.minConfidence],
1140
1543
  enabled: opts.enabled !== false,
@@ -1148,7 +1551,7 @@ function useAeroTracks(opts = {}) {
1148
1551
  return result;
1149
1552
  }
1150
1553
  });
1151
- useEffect2(() => {
1554
+ useEffect3(() => {
1152
1555
  if (opts.enabled === false) return;
1153
1556
  const actions = useTrackStore.getState();
1154
1557
  unsubRef.current = client.tracks.subscribe(
@@ -1190,7 +1593,7 @@ function useAeroTracks(opts = {}) {
1190
1593
  }
1191
1594
  var useTracks = useAeroTracks;
1192
1595
  function useAeroTrack(trackId) {
1193
- const track = useTrackStore(useMemo2(() => selectTrack(trackId), [trackId]));
1596
+ const track = useTrackStore(useMemo3(() => selectTrack(trackId), [trackId]));
1194
1597
  return { track };
1195
1598
  }
1196
1599
  function useAeroSnapshot(opts = {}) {
@@ -1283,6 +1686,7 @@ export {
1283
1686
  useSourceHealth,
1284
1687
  useTrackCount,
1285
1688
  useTrackHistory,
1689
+ useTrackMotion,
1286
1690
  useTrackStore,
1287
1691
  useTracks,
1288
1692
  useWebhooks