@squawk/adsbtop 0.2.0

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.
Files changed (74) hide show
  1. package/README.md +65 -0
  2. package/dist/aircraft-state.d.ts +77 -0
  3. package/dist/aircraft-state.d.ts.map +1 -0
  4. package/dist/aircraft-state.js +87 -0
  5. package/dist/app.d.ts +41 -0
  6. package/dist/app.d.ts.map +1 -0
  7. package/dist/app.js +166 -0
  8. package/dist/cli-args.d.ts +43 -0
  9. package/dist/cli-args.d.ts.map +1 -0
  10. package/dist/cli-args.js +150 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +17 -0
  14. package/dist/columns.d.ts +84 -0
  15. package/dist/columns.d.ts.map +1 -0
  16. package/dist/columns.js +203 -0
  17. package/dist/components/aircraft-table.d.ts +27 -0
  18. package/dist/components/aircraft-table.d.ts.map +1 -0
  19. package/dist/components/aircraft-table.js +83 -0
  20. package/dist/components/detail-view.d.ts +20 -0
  21. package/dist/components/detail-view.d.ts.map +1 -0
  22. package/dist/components/detail-view.js +14 -0
  23. package/dist/components/help-overlay.d.ts +4 -0
  24. package/dist/components/help-overlay.d.ts.map +1 -0
  25. package/dist/components/help-overlay.js +22 -0
  26. package/dist/components/hotkey-bar.d.ts +21 -0
  27. package/dist/components/hotkey-bar.d.ts.map +1 -0
  28. package/dist/components/hotkey-bar.js +28 -0
  29. package/dist/components/messages-panel.d.ts +27 -0
  30. package/dist/components/messages-panel.d.ts.map +1 -0
  31. package/dist/components/messages-panel.js +17 -0
  32. package/dist/components/search-bar.d.ts +19 -0
  33. package/dist/components/search-bar.d.ts.map +1 -0
  34. package/dist/components/search-bar.js +13 -0
  35. package/dist/components/status-header.d.ts +17 -0
  36. package/dist/components/status-header.d.ts.map +1 -0
  37. package/dist/components/status-header.js +14 -0
  38. package/dist/create-feed.d.ts +33 -0
  39. package/dist/create-feed.d.ts.map +1 -0
  40. package/dist/create-feed.js +40 -0
  41. package/dist/detail-fields.d.ts +23 -0
  42. package/dist/detail-fields.d.ts.map +1 -0
  43. package/dist/detail-fields.js +68 -0
  44. package/dist/format.d.ts +91 -0
  45. package/dist/format.d.ts.map +1 -0
  46. package/dist/format.js +138 -0
  47. package/dist/location.d.ts +20 -0
  48. package/dist/location.d.ts.map +1 -0
  49. package/dist/location.js +29 -0
  50. package/dist/registration-cache.d.ts +23 -0
  51. package/dist/registration-cache.d.ts.map +1 -0
  52. package/dist/registration-cache.js +24 -0
  53. package/dist/search.d.ts +27 -0
  54. package/dist/search.d.ts.map +1 -0
  55. package/dist/search.js +54 -0
  56. package/dist/selection.d.ts +25 -0
  57. package/dist/selection.d.ts.map +1 -0
  58. package/dist/selection.js +38 -0
  59. package/dist/sparkline.d.ts +26 -0
  60. package/dist/sparkline.d.ts.map +1 -0
  61. package/dist/sparkline.js +62 -0
  62. package/dist/status-line.d.ts +28 -0
  63. package/dist/status-line.d.ts.map +1 -0
  64. package/dist/status-line.js +18 -0
  65. package/dist/test-utils.d.ts +23 -0
  66. package/dist/test-utils.d.ts.map +1 -0
  67. package/dist/test-utils.js +35 -0
  68. package/dist/use-aircraft-feed.d.ts +28 -0
  69. package/dist/use-aircraft-feed.d.ts.map +1 -0
  70. package/dist/use-aircraft-feed.js +68 -0
  71. package/dist/use-icao-registry.d.ts +27 -0
  72. package/dist/use-icao-registry.d.ts.map +1 -0
  73. package/dist/use-icao-registry.js +37 -0
  74. package/package.json +58 -0
@@ -0,0 +1,25 @@
1
+ import type { Aircraft } from '@squawk/types';
2
+ /**
3
+ * Finds the index of the aircraft currently identified by `selectedIcaoHex`
4
+ * within `aircraft`.
5
+ *
6
+ * @param aircraft - The currently displayed aircraft, in display order.
7
+ * @param selectedIcaoHex - The selected aircraft's ICAO hex, or undefined if none is selected.
8
+ * @returns The index of the selected aircraft, or -1 if none is selected or it is no longer present.
9
+ */
10
+ export declare function findSelectedIndex(aircraft: readonly Aircraft[], selectedIcaoHex: string | undefined): number;
11
+ /**
12
+ * Computes the ICAO hex to select after moving the cursor by `delta` rows
13
+ * from the current selection. Clamped at the first/last row rather than
14
+ * wrapping, matching `htop`'s arrow-key behavior. If the current selection
15
+ * is unset or no longer present in `aircraft` (e.g. the aircraft was lost),
16
+ * resets to the first row regardless of `delta` - simpler and more
17
+ * predictable than trying to reconstruct "where it would have moved to".
18
+ *
19
+ * @param aircraft - The currently displayed aircraft, in display order.
20
+ * @param selectedIcaoHex - The currently selected aircraft's ICAO hex, or undefined if none is selected.
21
+ * @param delta - Rows to move: -1 for up, 1 for down.
22
+ * @returns The ICAO hex to select next, or undefined if `aircraft` is empty.
23
+ */
24
+ export declare function moveSelection(aircraft: readonly Aircraft[], selectedIcaoHex: string | undefined, delta: -1 | 1): string | undefined;
25
+ //# sourceMappingURL=selection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../src/selection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,SAAS,QAAQ,EAAE,EAC7B,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,MAAM,CAKR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,SAAS,QAAQ,EAAE,EAC7B,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,GACZ,MAAM,GAAG,SAAS,CAUpB"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Finds the index of the aircraft currently identified by `selectedIcaoHex`
3
+ * within `aircraft`.
4
+ *
5
+ * @param aircraft - The currently displayed aircraft, in display order.
6
+ * @param selectedIcaoHex - The selected aircraft's ICAO hex, or undefined if none is selected.
7
+ * @returns The index of the selected aircraft, or -1 if none is selected or it is no longer present.
8
+ */
9
+ export function findSelectedIndex(aircraft, selectedIcaoHex) {
10
+ if (selectedIcaoHex === undefined) {
11
+ return -1;
12
+ }
13
+ return aircraft.findIndex((candidate) => candidate.icaoHex === selectedIcaoHex);
14
+ }
15
+ /**
16
+ * Computes the ICAO hex to select after moving the cursor by `delta` rows
17
+ * from the current selection. Clamped at the first/last row rather than
18
+ * wrapping, matching `htop`'s arrow-key behavior. If the current selection
19
+ * is unset or no longer present in `aircraft` (e.g. the aircraft was lost),
20
+ * resets to the first row regardless of `delta` - simpler and more
21
+ * predictable than trying to reconstruct "where it would have moved to".
22
+ *
23
+ * @param aircraft - The currently displayed aircraft, in display order.
24
+ * @param selectedIcaoHex - The currently selected aircraft's ICAO hex, or undefined if none is selected.
25
+ * @param delta - Rows to move: -1 for up, 1 for down.
26
+ * @returns The ICAO hex to select next, or undefined if `aircraft` is empty.
27
+ */
28
+ export function moveSelection(aircraft, selectedIcaoHex, delta) {
29
+ if (aircraft.length === 0) {
30
+ return undefined;
31
+ }
32
+ const currentIndex = findSelectedIndex(aircraft, selectedIcaoHex);
33
+ if (currentIndex === -1) {
34
+ return aircraft[0]?.icaoHex;
35
+ }
36
+ const nextIndex = Math.min(Math.max(currentIndex + delta, 0), aircraft.length - 1);
37
+ return aircraft[nextIndex]?.icaoHex;
38
+ }
@@ -0,0 +1,26 @@
1
+ import type { PositionHistoryEntry } from '@squawk/adsb-feed';
2
+ /**
3
+ * A two-row altitude sparkline. Stacking two block-character rows doubles
4
+ * the vertical resolution of a single row (16 distinguishable levels instead
5
+ * of 8) - a single row's eighth-block characters top out at 8 heights, not
6
+ * enough to show real definition once a flight settles into a shallow climb
7
+ * or a cruise segment with only minor wander.
8
+ */
9
+ export interface AltitudeSparkline {
10
+ /** Top row: each column's fill above the row split (blank where the column doesn't reach it). */
11
+ topRow: string;
12
+ /** Bottom row: each column's fill up to the row split. */
13
+ bottomRow: string;
14
+ }
15
+ /**
16
+ * Builds a two-row altitude sparkline from a position history, oldest sample
17
+ * first. Barometric altitude is preferred per sample, geometric as a
18
+ * fallback - matching the table's altitude column precedence. Samples with
19
+ * neither field are skipped rather than breaking the line. Only the most
20
+ * recent {@link MAX_SPARKLINE_SAMPLES} are rendered.
21
+ *
22
+ * @param history - Position samples for one aircraft, oldest first (as returned by `AircraftFeed.getPositionHistory`).
23
+ * @returns The two rows, each scaled between the shown window's min and max altitude, or undefined if no sample carries an altitude.
24
+ */
25
+ export declare function buildAltitudeSparkline(history: readonly PositionHistoryEntry[]): AltitudeSparkline | undefined;
26
+ //# sourceMappingURL=sparkline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sparkline.d.ts","sourceRoot":"","sources":["../src/sparkline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAiB9D;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,iGAAiG;IACjG,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;CACnB;AAYD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,SAAS,oBAAoB,EAAE,GACvC,iBAAiB,GAAG,SAAS,CAmC/B"}
@@ -0,0 +1,62 @@
1
+ /** Unicode block characters, one per eighth-row fill level (index 0 = 1/8 filled, index 7 = fully filled). */
2
+ const SPARKLINE_LEVELS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
3
+ /** Combined vertical levels across both rows (8 per row) - double a single row's resolution. */
4
+ const TOTAL_LEVELS = SPARKLINE_LEVELS.length * 2;
5
+ /**
6
+ * Maximum samples rendered, most recent first truncated from the front of
7
+ * `history`. A sparkline is a fixed-size recent-trend glyph, not a full
8
+ * session dump - without this cap the line grows by one character per
9
+ * position update for as long as an aircraft stays tracked, eventually
10
+ * overflowing the terminal width and wrapping.
11
+ */
12
+ const MAX_SPARKLINE_SAMPLES = 60;
13
+ /**
14
+ * Renders `units` (0-8) as one row's block character, blank when 0.
15
+ *
16
+ * @param units - Eighths filled in this row, 0-8.
17
+ * @returns The block character for `units`, or a space when `units` is 0.
18
+ */
19
+ function rowChar(units) {
20
+ return units <= 0 ? ' ' : (SPARKLINE_LEVELS[Math.min(units, SPARKLINE_LEVELS.length) - 1] ?? '');
21
+ }
22
+ /**
23
+ * Builds a two-row altitude sparkline from a position history, oldest sample
24
+ * first. Barometric altitude is preferred per sample, geometric as a
25
+ * fallback - matching the table's altitude column precedence. Samples with
26
+ * neither field are skipped rather than breaking the line. Only the most
27
+ * recent {@link MAX_SPARKLINE_SAMPLES} are rendered.
28
+ *
29
+ * @param history - Position samples for one aircraft, oldest first (as returned by `AircraftFeed.getPositionHistory`).
30
+ * @returns The two rows, each scaled between the shown window's min and max altitude, or undefined if no sample carries an altitude.
31
+ */
32
+ export function buildAltitudeSparkline(history) {
33
+ const recent = history.slice(-MAX_SPARKLINE_SAMPLES);
34
+ const altitudes = recent
35
+ .map((entry) => entry.position.baroAltitudeFt ?? entry.position.geoAltitudeFt)
36
+ .filter((altitude) => altitude !== undefined);
37
+ if (altitudes.length === 0) {
38
+ return undefined;
39
+ }
40
+ const min = Math.min(...altitudes);
41
+ const max = Math.max(...altitudes);
42
+ if (min === max) {
43
+ // No range to scale against - render a flat mid-level bar on the bottom
44
+ // row only, leaving the top row blank. Matches the single-row version's
45
+ // convention for a value that never moved, and keeps "the top row is
46
+ // lit" meaning "real variation exists" rather than something a
47
+ // perfectly flat run can also trigger.
48
+ const flatBar = SPARKLINE_LEVELS[Math.floor(SPARKLINE_LEVELS.length / 2)] ?? '';
49
+ return { topRow: ' '.repeat(altitudes.length), bottomRow: flatBar.repeat(altitudes.length) };
50
+ }
51
+ const topChars = [];
52
+ const bottomChars = [];
53
+ for (const altitude of altitudes) {
54
+ const ratio = (altitude - min) / (max - min);
55
+ const level = Math.min(TOTAL_LEVELS - 1, Math.floor(ratio * TOTAL_LEVELS));
56
+ const bottomUnits = Math.min(level + 1, SPARKLINE_LEVELS.length);
57
+ const topUnits = level + 1 - bottomUnits;
58
+ bottomChars.push(rowChar(bottomUnits));
59
+ topChars.push(rowChar(topUnits));
60
+ }
61
+ return { topRow: topChars.join(''), bottomRow: bottomChars.join('') };
62
+ }
@@ -0,0 +1,28 @@
1
+ import type { FeedSource } from './cli-args.js';
2
+ /** Inputs for {@link formatStatusLine}. */
3
+ export interface StatusLineInfo {
4
+ /** Feed source currently in use. */
5
+ source: FeedSource;
6
+ /** Station host being connected to. */
7
+ host: string;
8
+ /** Station port being connected to. */
9
+ port: number;
10
+ /** Number of aircraft currently tracked. */
11
+ aircraftCount: number;
12
+ /** Update events observed in roughly the last second. */
13
+ messageRatePerSec: number;
14
+ /** Unix epoch ms of the most recent update, or undefined if none has arrived yet. */
15
+ lastMessageAt: number | undefined;
16
+ /** Current time, for the "last update" age. */
17
+ nowMs: number;
18
+ }
19
+ /**
20
+ * Builds the single-line connection/activity summary shown in the status
21
+ * header. A pure string builder, kept separate from the Ink component so it
22
+ * is directly unit-testable without a render harness.
23
+ *
24
+ * @param info - The connection and activity state to summarize.
25
+ * @returns The formatted status line, without any styling applied.
26
+ */
27
+ export declare function formatStatusLine(info: StatusLineInfo): string;
28
+ //# sourceMappingURL=status-line.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status-line.d.ts","sourceRoot":"","sources":["../src/status-line.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,oCAAoC;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAC;IACtB,yDAAyD;IACzD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qFAAqF;IACrF,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAW7D"}
@@ -0,0 +1,18 @@
1
+ import { formatAge } from './format.js';
2
+ /**
3
+ * Builds the single-line connection/activity summary shown in the status
4
+ * header. A pure string builder, kept separate from the Ink component so it
5
+ * is directly unit-testable without a render harness.
6
+ *
7
+ * @param info - The connection and activity state to summarize.
8
+ * @returns The formatted status line, without any styling applied.
9
+ */
10
+ export function formatStatusLine(info) {
11
+ const lastUpdate = info.lastMessageAt === undefined
12
+ ? 'none yet'
13
+ : `${formatAge(info.lastMessageAt, info.nowMs)} ago`;
14
+ return (`source: ${info.source} ${info.host}:${info.port} | ` +
15
+ `aircraft: ${info.aircraftCount} | ` +
16
+ `msgs/s: ${info.messageRatePerSec} | ` +
17
+ `last update: ${lastUpdate}`);
18
+ }
@@ -0,0 +1,23 @@
1
+ import type { AircraftFeed } from '@squawk/adsb-feed';
2
+ import type { AircraftRegistration } from '@squawk/types';
3
+ import type { RegistryDataLoader } from './use-icao-registry.js';
4
+ /** A fake {@link AircraftFeed} for tests: a real `EventTarget` so production code can dispatch events on it normally, with `start`/`stop` call counts and stubbed query methods. */
5
+ export interface FakeAircraftFeed extends AircraftFeed {
6
+ /** Number of times `start()` has been called. */
7
+ startCalls: number;
8
+ /** Number of times `stop()` has been called. */
9
+ stopCalls: number;
10
+ }
11
+ /**
12
+ * Creates a {@link FakeAircraftFeed} with no real socket/HTTP behavior, for
13
+ * tests that need to dispatch `aircraft:new`/`aircraft:update`/`aircraft:lost`
14
+ * events without a live dump1090-fa station.
15
+ */
16
+ export declare function createFakeAircraftFeed(): FakeAircraftFeed;
17
+ /**
18
+ * Creates a {@link RegistryDataLoader} resolving to `records` (empty by
19
+ * default), for tests that render `App` without pulling in the real
20
+ * `@squawk/icao-registry-data` package's ~40MB in-memory dataset.
21
+ */
22
+ export declare function createFakeRegistryDataLoader(records?: AircraftRegistration[]): RegistryDataLoader;
23
+ //# sourceMappingURL=test-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAwB,MAAM,mBAAmB,CAAC;AAC5E,OAAO,KAAK,EAAY,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE,oLAAoL;AACpL,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAqBzD;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,oBAAoB,EAAO,GACnC,kBAAkB,CAEpB"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Creates a {@link FakeAircraftFeed} with no real socket/HTTP behavior, for
3
+ * tests that need to dispatch `aircraft:new`/`aircraft:update`/`aircraft:lost`
4
+ * events without a live dump1090-fa station.
5
+ */
6
+ export function createFakeAircraftFeed() {
7
+ const feed = Object.assign(new EventTarget(), {
8
+ startCalls: 0,
9
+ stopCalls: 0,
10
+ start() {
11
+ feed.startCalls += 1;
12
+ },
13
+ stop() {
14
+ feed.stopCalls += 1;
15
+ },
16
+ getAircraft() {
17
+ return undefined;
18
+ },
19
+ getAllAircraft() {
20
+ return [];
21
+ },
22
+ getPositionHistory() {
23
+ return [];
24
+ },
25
+ });
26
+ return feed;
27
+ }
28
+ /**
29
+ * Creates a {@link RegistryDataLoader} resolving to `records` (empty by
30
+ * default), for tests that render `App` without pulling in the real
31
+ * `@squawk/icao-registry-data` package's ~40MB in-memory dataset.
32
+ */
33
+ export function createFakeRegistryDataLoader(records = []) {
34
+ return () => Promise.resolve({ usBundledRegistry: { records } });
35
+ }
@@ -0,0 +1,28 @@
1
+ import type { AircraftFeed } from '@squawk/adsb-feed';
2
+ import type { Aircraft } from '@squawk/types';
3
+ import type { MessageLogEntry } from './aircraft-state.js';
4
+ /** Live view of an `AircraftFeed`'s tracked aircraft and activity stats. */
5
+ export interface AircraftFeedView {
6
+ /** Currently tracked aircraft, in no particular order - sort before display. */
7
+ aircraft: Aircraft[];
8
+ /** Total update events observed since the feed started. */
9
+ messageCount: number;
10
+ /** Unix epoch ms of the most recent update, or undefined if none has arrived yet. */
11
+ lastMessageAt: number | undefined;
12
+ /** Update events observed in roughly the last second. */
13
+ messageRatePerSec: number;
14
+ /** Every event type, oldest first, for the `[M]essages` panel's `all` verbosity. */
15
+ messageLog: MessageLogEntry[];
16
+ /** `aircraft:new`/`aircraft:lost` events only, oldest first, for the panel's default `newAndLost` verbosity - capped independently of `messageLog` so update volume can't evict a still-relevant entry. */
17
+ newAndLostLog: MessageLogEntry[];
18
+ }
19
+ /**
20
+ * Subscribes to `feed`'s aircraft events for the component's lifetime,
21
+ * starting it on mount and stopping it on unmount, and accumulates tracked
22
+ * aircraft plus activity stats via the pure `aircraftStateReducer`.
23
+ *
24
+ * @param feed - The feed to subscribe to. Changing the reference tears down the old subscription and starts a new one.
25
+ * @returns The current aircraft list and activity stats, updated as events arrive.
26
+ */
27
+ export declare function useAircraftFeed(feed: AircraftFeed): AircraftFeedView;
28
+ //# sourceMappingURL=use-aircraft-feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-aircraft-feed.d.ts","sourceRoot":"","sources":["../src/use-aircraft-feed.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAK3D,4EAA4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,yDAAyD;IACzD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,2MAA2M;IAC3M,aAAa,EAAE,eAAe,EAAE,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,CA8DpE"}
@@ -0,0 +1,68 @@
1
+ import { useEffect, useMemo, useReducer, useRef, useState } from 'react';
2
+ import { aircraftStateReducer, initialAircraftState } from './aircraft-state.js';
3
+ /** How often {@link useAircraftFeed} recomputes `messageRatePerSec`. */
4
+ const RATE_SAMPLE_INTERVAL_MS = 1000;
5
+ /**
6
+ * Subscribes to `feed`'s aircraft events for the component's lifetime,
7
+ * starting it on mount and stopping it on unmount, and accumulates tracked
8
+ * aircraft plus activity stats via the pure `aircraftStateReducer`.
9
+ *
10
+ * @param feed - The feed to subscribe to. Changing the reference tears down the old subscription and starts a new one.
11
+ * @returns The current aircraft list and activity stats, updated as events arrive.
12
+ */
13
+ export function useAircraftFeed(feed) {
14
+ const [state, dispatch] = useReducer(aircraftStateReducer, initialAircraftState);
15
+ const [messageRatePerSec, setMessageRatePerSec] = useState(0);
16
+ const messageCountRef = useRef(state.messageCount);
17
+ useEffect(() => {
18
+ messageCountRef.current = state.messageCount;
19
+ }, [state.messageCount]);
20
+ useEffect(() => {
21
+ function handleNew(event) {
22
+ const { aircraft } = event.detail;
23
+ dispatch({ type: 'message', kind: 'new', aircraft, at: Date.now() });
24
+ }
25
+ function handleUpdate(event) {
26
+ const { aircraft } = event.detail;
27
+ dispatch({ type: 'message', kind: 'update', aircraft, at: Date.now() });
28
+ }
29
+ function handleLost(event) {
30
+ const { icaoHex, lastAircraft } = event.detail;
31
+ dispatch({ type: 'lost', icaoHex, callsign: lastAircraft.callsign, at: Date.now() });
32
+ }
33
+ feed.addEventListener('aircraft:new', handleNew);
34
+ feed.addEventListener('aircraft:update', handleUpdate);
35
+ feed.addEventListener('aircraft:lost', handleLost);
36
+ feed.start();
37
+ return () => {
38
+ feed.removeEventListener('aircraft:new', handleNew);
39
+ feed.removeEventListener('aircraft:update', handleUpdate);
40
+ feed.removeEventListener('aircraft:lost', handleLost);
41
+ feed.stop();
42
+ };
43
+ }, [feed]);
44
+ useEffect(() => {
45
+ let lastCount = messageCountRef.current;
46
+ const handle = setInterval(() => {
47
+ setMessageRatePerSec(messageCountRef.current - lastCount);
48
+ lastCount = messageCountRef.current;
49
+ }, RATE_SAMPLE_INTERVAL_MS);
50
+ return () => {
51
+ clearInterval(handle);
52
+ };
53
+ }, []);
54
+ // Memoized against the Map reference (which the reducer only replaces on a
55
+ // real message/lost action) rather than recomputed on every render - a
56
+ // fresh array on every render would make `aircraft` look "changed" to any
57
+ // consumer's effect/memo dependency array even when nothing happened, e.g.
58
+ // on the clock-tick re-renders the app's age column relies on.
59
+ const aircraft = useMemo(() => Array.from(state.aircraftByHex.values()), [state.aircraftByHex]);
60
+ return {
61
+ aircraft,
62
+ messageCount: state.messageCount,
63
+ lastMessageAt: state.lastMessageAt,
64
+ messageRatePerSec,
65
+ messageLog: state.messageLog,
66
+ newAndLostLog: state.newAndLostLog,
67
+ };
68
+ }
@@ -0,0 +1,27 @@
1
+ import type { IcaoRegistry } from '@squawk/icao-registry';
2
+ import type { AircraftRegistration } from '@squawk/types';
3
+ /** The subset of `@squawk/icao-registry-data`'s module exports this hook needs. */
4
+ export interface RegistryDataModule {
5
+ /** The bundled US aircraft registry dataset. */
6
+ usBundledRegistry: {
7
+ /** Raw registration records, keyed by `icaoHex` once loaded into a resolver. */
8
+ records: AircraftRegistration[];
9
+ };
10
+ }
11
+ /** Loads the bundled registry dataset module - overridable so tests don't import the real ~40MB dataset. */
12
+ export type RegistryDataLoader = () => Promise<RegistryDataModule>;
13
+ /**
14
+ * Lazily loads `@squawk/icao-registry-data` and builds an `IcaoRegistry`
15
+ * after mount, so the dataset's decompress-and-parse cost doesn't block
16
+ * adsbtop's first paint. `@squawk/icao-registry-data` is a hard dependency
17
+ * (registration lookup is adsbtop's headline enrichment feature, unlike
18
+ * `@squawk/mcp`'s optional-peer treatment of the same dataset, where it's
19
+ * one tool among many) - a load failure is unexpected, and degrades to
20
+ * leaving registration columns/fields at `-` rather than crashing the live
21
+ * dashboard over a non-core enhancement.
22
+ *
23
+ * @param loadData - Loader for the bundled dataset module. Defaults to a real dynamic import of `@squawk/icao-registry-data`; overridable in tests.
24
+ * @returns The ready-to-query registry, or undefined while loading (or if loading failed).
25
+ */
26
+ export declare function useIcaoRegistry(loadData?: RegistryDataLoader): IcaoRegistry | undefined;
27
+ //# sourceMappingURL=use-icao-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-icao-registry.d.ts","sourceRoot":"","sources":["../src/use-icao-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,kBAAkB;IACjC,gDAAgD;IAChD,iBAAiB,EAAE;QACjB,gFAAgF;QAChF,OAAO,EAAE,oBAAoB,EAAE,CAAC;KACjC,CAAC;CACH;AAED,4GAA4G;AAC5G,MAAM,MAAM,kBAAkB,GAAG,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAInE;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,GAAE,kBAAkC,GAC3C,YAAY,GAAG,SAAS,CAsB1B"}
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { createIcaoRegistry } from '@squawk/icao-registry';
3
+ const defaultLoader = () => import('@squawk/icao-registry-data');
4
+ /**
5
+ * Lazily loads `@squawk/icao-registry-data` and builds an `IcaoRegistry`
6
+ * after mount, so the dataset's decompress-and-parse cost doesn't block
7
+ * adsbtop's first paint. `@squawk/icao-registry-data` is a hard dependency
8
+ * (registration lookup is adsbtop's headline enrichment feature, unlike
9
+ * `@squawk/mcp`'s optional-peer treatment of the same dataset, where it's
10
+ * one tool among many) - a load failure is unexpected, and degrades to
11
+ * leaving registration columns/fields at `-` rather than crashing the live
12
+ * dashboard over a non-core enhancement.
13
+ *
14
+ * @param loadData - Loader for the bundled dataset module. Defaults to a real dynamic import of `@squawk/icao-registry-data`; overridable in tests.
15
+ * @returns The ready-to-query registry, or undefined while loading (or if loading failed).
16
+ */
17
+ export function useIcaoRegistry(loadData = defaultLoader) {
18
+ const [registry, setRegistry] = useState(undefined);
19
+ useEffect(() => {
20
+ let cancelled = false;
21
+ loadData()
22
+ .then(({ usBundledRegistry }) => {
23
+ if (!cancelled) {
24
+ setRegistry(createIcaoRegistry({ data: usBundledRegistry.records }));
25
+ }
26
+ })
27
+ .catch(() => {
28
+ // Registration enrichment is a display enhancement, not core
29
+ // tracking functionality - leave the registry unset rather than
30
+ // crash the dashboard over a failed load.
31
+ });
32
+ return () => {
33
+ cancelled = true;
34
+ };
35
+ }, [loadData]);
36
+ return registry;
37
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@squawk/adsbtop",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Terminal dashboard for live ADS-B aircraft tracking, built on @squawk/adsb-feed",
6
+ "license": "MIT",
7
+ "author": "Neil Cochran",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/neilcochran/squawk.git",
11
+ "directory": "apps/adsbtop"
12
+ },
13
+ "homepage": "https://github.com/neilcochran/squawk",
14
+ "engines": {
15
+ "node": ">=22"
16
+ },
17
+ "bin": {
18
+ "adsbtop": "./dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "!dist/**/*.spec.*"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc && node -e \"require('fs').chmodSync('dist/cli.js', 0o755)\"",
26
+ "test": "vitest run",
27
+ "test:coverage": "vitest run --coverage",
28
+ "lint": "tsc --noEmit && eslint src"
29
+ },
30
+ "dependencies": {
31
+ "@squawk/adsb-feed": "^0.2.0",
32
+ "@squawk/geo": "^0.4.9",
33
+ "@squawk/icao-registry": "^0.5.7",
34
+ "@squawk/icao-registry-data": "^0.8.11",
35
+ "@squawk/types": "^0.8.6",
36
+ "ink": "^7.1.1",
37
+ "ink-text-input": "^6.0.0",
38
+ "react": "^19.2.8"
39
+ },
40
+ "keywords": [
41
+ "aviation",
42
+ "adsb",
43
+ "ads-b",
44
+ "aircraft-tracking",
45
+ "terminal",
46
+ "tui",
47
+ "cli"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^26.4.0",
54
+ "@types/react": "^19.2.18",
55
+ "eslint-plugin-react-hooks": "^7.1.1",
56
+ "ink-testing-library": "^4.0.0"
57
+ }
58
+ }