@aeroswarm/client 0.1.11 → 0.1.12

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.
@@ -23,11 +23,13 @@ var AeroWsClient = class {
23
23
  lastSeq = -1;
24
24
  lastHeartbeat = Date.now();
25
25
  heartbeatTimer = null;
26
+ reconnectTimer = null;
26
27
  status = "idle";
27
28
  connect(opts, callbacks) {
28
29
  this.lastSubscribe = opts;
29
30
  this.callbacks = callbacks;
30
31
  this.stopped = false;
32
+ this._clearReconnectTimer();
31
33
  this._setStatus("connecting");
32
34
  this._connect();
33
35
  }
@@ -38,10 +40,27 @@ var AeroWsClient = class {
38
40
  disconnect() {
39
41
  this.stopped = true;
40
42
  this._clearHeartbeatTimer();
41
- this.ws?.close();
42
- this.ws = null;
43
+ this._clearReconnectTimer();
44
+ this._dropSocket();
43
45
  this._setStatus("offline");
44
46
  }
47
+ /**
48
+ * Force a fresh connection now, skipping any backoff wait. Re-subscribes with
49
+ * the last `connect()` options (the server then sends a new snapshot).
50
+ * Also resumes after `disconnect()`. No-op if `connect()` was never called.
51
+ * Returns false in that no-op case.
52
+ */
53
+ reconnect() {
54
+ if (!this.lastSubscribe) return false;
55
+ this.stopped = false;
56
+ this.attempt = 0;
57
+ this._clearHeartbeatTimer();
58
+ this._clearReconnectTimer();
59
+ this._dropSocket();
60
+ this._setStatus("reconnecting");
61
+ this._connect();
62
+ return true;
63
+ }
45
64
  getStatus() {
46
65
  return this.status;
47
66
  }
@@ -50,8 +69,17 @@ var AeroWsClient = class {
50
69
  this.status = s;
51
70
  this.callbacks.onStatusChange?.(s);
52
71
  }
72
+ /** Close the current socket without letting its onclose schedule a reconnect. */
73
+ _dropSocket() {
74
+ const ws = this.ws;
75
+ this.ws = null;
76
+ if (!ws) return;
77
+ ws.onopen = ws.onmessage = ws.onclose = ws.onerror = null;
78
+ ws.close();
79
+ }
53
80
  _connect() {
54
81
  const wsUrl = `${this.url}?token=${encodeURIComponent(this.token)}`;
82
+ this._dropSocket();
55
83
  try {
56
84
  this.ws = new WebSocket(wsUrl);
57
85
  this.ws.binaryType = "arraybuffer";
@@ -167,10 +195,18 @@ var AeroWsClient = class {
167
195
  _scheduleReconnect() {
168
196
  const delay = Math.min(1e3 * 2 ** this.attempt, this.maxReconnectDelay);
169
197
  this.attempt++;
170
- setTimeout(() => {
198
+ this._clearReconnectTimer();
199
+ this.reconnectTimer = setTimeout(() => {
200
+ this.reconnectTimer = null;
171
201
  if (!this.stopped) this._connect();
172
202
  }, delay);
173
203
  }
204
+ _clearReconnectTimer() {
205
+ if (this.reconnectTimer) {
206
+ clearTimeout(this.reconnectTimer);
207
+ this.reconnectTimer = null;
208
+ }
209
+ }
174
210
  };
175
211
 
176
212
  // src/sdk/websocket/replay.ts
@@ -320,6 +356,28 @@ var AeroRestClient = class {
320
356
  const qs = search.size ? `?${search.toString()}` : "";
321
357
  return this.req("GET", `/v1/tracks/current${qs}`);
322
358
  }
359
+ /** Flight plans received from AFTN feeds (FPL/CHG/DLA/DEP/ARR/CNL folded per flight). */
360
+ listFlightPlans(params) {
361
+ const search = new URLSearchParams();
362
+ if (params?.airportId) search.set("airportId", params.airportId);
363
+ if (params?.status) search.set("status", params.status);
364
+ if (params?.callsign) search.set("callsign", params.callsign);
365
+ if (params?.limit != null) search.set("limit", String(params.limit));
366
+ const qs = search.size ? `?${search.toString()}` : "";
367
+ return this.req("GET", `/v1/flight-plans${qs}`);
368
+ }
369
+ getFlightPlan(id) {
370
+ return this.req("GET", `/v1/flight-plans/${encodeURIComponent(id)}`);
371
+ }
372
+ /** Raw AFTN messages (every parsed ATS message, newest first — the input
373
+ * side of the flight-plan pipeline). */
374
+ listAftnMessages(params) {
375
+ const search = new URLSearchParams();
376
+ if (params?.airportId) search.set("airportId", params.airportId);
377
+ if (params?.limit != null) search.set("limit", String(params.limit));
378
+ const qs = search.size ? `?${search.toString()}` : "";
379
+ return this.req("GET", `/v1/aftn/messages${qs}`);
380
+ }
323
381
  health() {
324
382
  return this.req("GET", "/health");
325
383
  }
@@ -351,6 +409,38 @@ var AeroGraphQLClient = class {
351
409
 
352
410
  // src/sdk/cache/index.ts
353
411
  import { isRenderableTrack } from "@aeroswarm/core/utils";
412
+
413
+ // src/shared/label/index.ts
414
+ var LABEL_COLORS = {
415
+ ARRIVAL: "#FACC15",
416
+ // yellow
417
+ DEPARTURE: "#22C55E",
418
+ // green
419
+ GROUND_VEHICLE: "#3B82F6",
420
+ // blue
421
+ LOST: "#FFFFFF",
422
+ // white
423
+ UNKNOWN: "#FFFFFF"
424
+ // white — no flight plan
425
+ };
426
+ function getLabelCategory(track) {
427
+ if (track.status === "lost") return "LOST";
428
+ if (track.targetType === "vehicle") return "GROUND_VEHICLE";
429
+ switch (track.flightPlan?.phase) {
430
+ case "ARRIVAL":
431
+ return "ARRIVAL";
432
+ case "DEPARTURE":
433
+ return "DEPARTURE";
434
+ default:
435
+ return "UNKNOWN";
436
+ }
437
+ }
438
+ function withLabel(track) {
439
+ const labelCategory = getLabelCategory(track);
440
+ return { ...track, labelCategory, labelColor: LABEL_COLORS[labelCategory] };
441
+ }
442
+
443
+ // src/sdk/cache/index.ts
354
444
  var TypedEmitter = class {
355
445
  listeners = /* @__PURE__ */ new Map();
356
446
  on(event, fn) {
@@ -364,15 +454,18 @@ var TypedEmitter = class {
364
454
  };
365
455
  var TrackCache = class extends TypedEmitter {
366
456
  tracks = /* @__PURE__ */ new Map();
457
+ /** Merges into the cached track and returns the labeled result. A "lost"
458
+ * track is evicted from the cache but still returned (labelCategory LOST). */
367
459
  applyUpdate(track) {
460
+ const merged = withLabel({ ...this.tracks.get(track.trackId), ...track });
368
461
  if (track.status === "lost") {
369
462
  this.tracks.delete(track.trackId);
370
463
  this.emit("removed", { trackId: track.trackId });
371
- return;
464
+ return merged;
372
465
  }
373
- const merged = { ...this.tracks.get(track.trackId), ...track };
374
466
  this.tracks.set(track.trackId, merged);
375
467
  this.emit("update", merged);
468
+ return merged;
376
469
  }
377
470
  applyRemoved(ev) {
378
471
  this.tracks.delete(ev.trackId);
@@ -381,9 +474,11 @@ var TrackCache = class extends TypedEmitter {
381
474
  applySnapshot(tracks) {
382
475
  this.tracks.clear();
383
476
  for (const t of tracks) {
384
- if (isRenderableTrack(t)) this.tracks.set(t.trackId, t);
477
+ if (isRenderableTrack(t)) this.tracks.set(t.trackId, withLabel(t));
385
478
  }
386
- this.emit("snapshot", Array.from(this.tracks.values()));
479
+ const all = Array.from(this.tracks.values());
480
+ this.emit("snapshot", all);
481
+ return all;
387
482
  }
388
483
  getAll() {
389
484
  return Array.from(this.tracks.values());
@@ -477,14 +572,14 @@ var AeroSwarm = class _AeroSwarm {
477
572
  callbacks.onStatusChange?.(s);
478
573
  },
479
574
  onSnapshot: (tracks) => {
480
- this.cache.applySnapshot(tracks);
575
+ const labeled = this.cache.applySnapshot(tracks);
481
576
  this._lastUpdateAt = Date.now();
482
- callbacks.onSnapshot?.(tracks);
577
+ callbacks.onSnapshot?.(labeled);
483
578
  },
484
579
  onTrackUpdate: (track) => {
485
- this.cache.applyUpdate(track);
580
+ const labeled = this.cache.applyUpdate(track);
486
581
  this._lastUpdateAt = Date.now();
487
- callbacks.onTrackUpdate?.(track);
582
+ callbacks.onTrackUpdate?.(labeled);
488
583
  },
489
584
  onTrackRemoved: (ev) => {
490
585
  this.cache.applyRemoved(ev);
@@ -503,12 +598,20 @@ var AeroSwarm = class _AeroSwarm {
503
598
  return () => this.wsClient.disconnect();
504
599
  },
505
600
  updateFilter: (opts) => this.wsClient.updateFilter(opts),
601
+ /** Drop the live socket and reconnect immediately (no backoff wait),
602
+ * re-subscribing with the last options. Returns false if nothing has
603
+ * been subscribed yet. */
604
+ reconnect: () => this.wsClient.reconnect(),
506
605
  stream: (opts) => {
507
606
  const queue = [];
508
607
  let resolve = null;
509
608
  let done = false;
510
609
  this.wsClient.connect({ encoding: "json", ...opts }, {
511
- onTrackUpdate: (t) => {
610
+ onSnapshot: (tracks) => {
611
+ this.cache.applySnapshot(tracks);
612
+ },
613
+ onTrackUpdate: (raw) => {
614
+ const t = this.cache.applyUpdate(raw);
512
615
  if (resolve) {
513
616
  resolve({ value: t, done: false });
514
617
  resolve = null;
@@ -549,6 +652,17 @@ var AeroSwarm = class _AeroSwarm {
549
652
  return this.trpc.tracks.history.query(input);
550
653
  }
551
654
  };
655
+ /** Flight plans received from AFTN feeds. Live tracks that correlate with a
656
+ * plan (Mode-S address, else callsign) also carry `track.flightPlan`. */
657
+ flightPlans = {
658
+ list: (input) => this.rest.listFlightPlans(input).then((r) => r.data.flightPlans),
659
+ get: (id) => this.rest.getFlightPlan(id).then((r) => r.data.flightPlan)
660
+ };
661
+ /** Raw AFTN/ATS messages received by AFTN feeds — the input side of the
662
+ * flight-plan pipeline (newest first, capped by the service). */
663
+ aftn = {
664
+ messages: (input) => this.rest.listAftnMessages(input).then((r) => r.data.messages)
665
+ };
552
666
  sources = {
553
667
  /** Registers a source and returns its token, tied to `airportId` — a
554
668
  * source is a sensor physically at one airport, so this is required.
@@ -669,6 +783,9 @@ var AeroSwarm = class _AeroSwarm {
669
783
  disconnect() {
670
784
  this.wsClient.disconnect();
671
785
  }
786
+ reconnect() {
787
+ return this.wsClient.reconnect();
788
+ }
672
789
  };
673
790
 
674
791
  // src/react/context/index.tsx
@@ -917,7 +1034,7 @@ function useTrackHistory(options) {
917
1034
  }
918
1035
 
919
1036
  // src/react/hooks/index.ts
920
- import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
1037
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
921
1038
  import { useQuery } from "@tanstack/react-query";
922
1039
  import { useShallow } from "zustand/react/shallow";
923
1040
 
@@ -938,14 +1055,14 @@ var useTrackStore = create()((set, get) => ({
938
1055
  degradedReason: null,
939
1056
  applyUpdate(track) {
940
1057
  const next = new Map(get().tracks);
941
- next.set(track.trackId, { ...next.get(track.trackId), ...track });
1058
+ next.set(track.trackId, withLabel({ ...next.get(track.trackId), ...track }));
942
1059
  set({ tracks: next, lastUpdateAt: Date.now() });
943
1060
  },
944
1061
  applyBatch(updates, removedIds) {
945
1062
  if (updates.length === 0 && removedIds.length === 0) return;
946
1063
  const next = new Map(get().tracks);
947
1064
  for (const track of updates) {
948
- next.set(track.trackId, { ...next.get(track.trackId), ...track });
1065
+ next.set(track.trackId, withLabel({ ...next.get(track.trackId), ...track }));
949
1066
  }
950
1067
  for (const id of removedIds) next.delete(id);
951
1068
  set({ tracks: next, lastUpdateAt: Date.now() });
@@ -958,7 +1075,7 @@ var useTrackStore = create()((set, get) => ({
958
1075
  applySnapshot(tracks) {
959
1076
  const next = /* @__PURE__ */ new Map();
960
1077
  for (const t of tracks) {
961
- if (isRenderableTrack2(t)) next.set(t.trackId, t);
1078
+ if (isRenderableTrack2(t)) next.set(t.trackId, withLabel(t));
962
1079
  }
963
1080
  set({ tracks: next, lastUpdateAt: Date.now() });
964
1081
  },
@@ -1057,6 +1174,7 @@ function useAeroTracks(opts = {}) {
1057
1174
  useTrackStore.getState().setStatus("idle");
1058
1175
  };
1059
1176
  }, [opts.bbox?.join(","), opts.source, opts.minConfidence, opts.maxUpdateHz, opts.enabled]);
1177
+ const reconnect = useCallback2(() => client.tracks.reconnect(), [client]);
1060
1178
  return {
1061
1179
  tracks,
1062
1180
  allTracks,
@@ -1066,7 +1184,8 @@ function useAeroTracks(opts = {}) {
1066
1184
  isDegraded,
1067
1185
  lastUpdateAt,
1068
1186
  error,
1069
- trackCount: tracks.length
1187
+ trackCount: tracks.length,
1188
+ reconnect
1070
1189
  };
1071
1190
  }
1072
1191
  var useTracks = useAeroTracks;