@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.
- package/README.md +537 -474
- package/dist/index.d.ts +695 -3
- package/dist/index.js +133 -13
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +10 -8
- package/dist/react/index.js +136 -17
- package/dist/react/index.js.map +1 -1
- package/dist/sdk/index.d.ts +3 -581
- package/dist/sdk/index.js +129 -12
- package/dist/sdk/index.js.map +1 -1
- package/package.json +2 -2
package/dist/react/index.js
CHANGED
|
@@ -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.
|
|
42
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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?.(
|
|
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?.(
|
|
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
|
-
|
|
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;
|