@aeroswarm/client 0.0.0-stage → 0.1.11

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.
@@ -0,0 +1,1171 @@
1
+ // src/react/context/index.tsx
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+
5
+ // src/sdk/websocket/index.ts
6
+ import { streamEnvelopeSchema, currentTrackSchema } from "@aeroswarm/core/validation";
7
+ var AeroWsClient = class {
8
+ constructor(url, token, maxReconnectDelay = 3e4, heartbeatTimeoutMs = 35e3) {
9
+ this.url = url;
10
+ this.token = token;
11
+ this.maxReconnectDelay = maxReconnectDelay;
12
+ this.heartbeatTimeoutMs = heartbeatTimeoutMs;
13
+ }
14
+ url;
15
+ token;
16
+ maxReconnectDelay;
17
+ heartbeatTimeoutMs;
18
+ ws = null;
19
+ attempt = 0;
20
+ stopped = false;
21
+ lastSubscribe = null;
22
+ callbacks = {};
23
+ lastSeq = -1;
24
+ lastHeartbeat = Date.now();
25
+ heartbeatTimer = null;
26
+ status = "idle";
27
+ connect(opts, callbacks) {
28
+ this.lastSubscribe = opts;
29
+ this.callbacks = callbacks;
30
+ this.stopped = false;
31
+ this._setStatus("connecting");
32
+ this._connect();
33
+ }
34
+ updateFilter(opts) {
35
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
36
+ this.ws.send(JSON.stringify({ type: "update-filter", ...opts }));
37
+ }
38
+ disconnect() {
39
+ this.stopped = true;
40
+ this._clearHeartbeatTimer();
41
+ this.ws?.close();
42
+ this.ws = null;
43
+ this._setStatus("offline");
44
+ }
45
+ getStatus() {
46
+ return this.status;
47
+ }
48
+ _setStatus(s) {
49
+ if (this.status === s) return;
50
+ this.status = s;
51
+ this.callbacks.onStatusChange?.(s);
52
+ }
53
+ _connect() {
54
+ const wsUrl = `${this.url}?token=${encodeURIComponent(this.token)}`;
55
+ try {
56
+ this.ws = new WebSocket(wsUrl);
57
+ this.ws.binaryType = "arraybuffer";
58
+ } catch {
59
+ this._scheduleReconnect();
60
+ return;
61
+ }
62
+ this.ws.onopen = () => {
63
+ this.attempt = 0;
64
+ this.lastSeq = -1;
65
+ this._setStatus("connected");
66
+ this._resetHeartbeatTimer();
67
+ if (this.lastSubscribe) {
68
+ this.ws.send(JSON.stringify({
69
+ type: "subscribe",
70
+ encoding: "json",
71
+ ...this.lastSubscribe
72
+ }));
73
+ }
74
+ };
75
+ this.ws.onmessage = (event) => {
76
+ this._handleRaw(event.data);
77
+ };
78
+ this.ws.onclose = () => {
79
+ this._clearHeartbeatTimer();
80
+ if (!this.stopped) {
81
+ this._setStatus("reconnecting");
82
+ this._scheduleReconnect();
83
+ }
84
+ };
85
+ this.ws.onerror = () => {
86
+ this.callbacks.onError?.({
87
+ code: "WS_ERROR",
88
+ message: "WebSocket connection error",
89
+ recoverable: true
90
+ });
91
+ };
92
+ }
93
+ _handleRaw(raw) {
94
+ this._resetHeartbeatTimer();
95
+ let parsed;
96
+ try {
97
+ const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
98
+ parsed = JSON.parse(text);
99
+ } catch {
100
+ return;
101
+ }
102
+ let msg;
103
+ const result = streamEnvelopeSchema.safeParse(parsed);
104
+ if (!result.success) {
105
+ const raw2 = parsed;
106
+ if (raw2["type"] === "snapshot" && Array.isArray(raw2["payload"]?.["tracks"])) {
107
+ const rawTracks = raw2["payload"]["tracks"];
108
+ const goodTracks = rawTracks.map((t) => currentTrackSchema.safeParse(t)).filter((r) => r.success).map((r) => r.data);
109
+ if (goodTracks.length > 0) this.callbacks.onSnapshot?.(goodTracks);
110
+ }
111
+ return;
112
+ }
113
+ msg = result.data;
114
+ if (msg.seq <= this.lastSeq && this.lastSeq > 0) return;
115
+ if (msg.seq > this.lastSeq + 10 && this.lastSeq > 0) {
116
+ this._requestSnapshot();
117
+ }
118
+ this.lastSeq = msg.seq;
119
+ switch (msg.type) {
120
+ case "snapshot":
121
+ this.callbacks.onSnapshot?.(msg.payload.tracks);
122
+ break;
123
+ case "track.updated":
124
+ this.callbacks.onTrackUpdate?.(msg.payload.track);
125
+ break;
126
+ case "track.removed":
127
+ this.callbacks.onTrackRemoved?.(msg.payload);
128
+ break;
129
+ case "source.status":
130
+ this.callbacks.onSourceStatus?.(msg.payload);
131
+ break;
132
+ case "stream.heartbeat":
133
+ this.lastHeartbeat = Date.now();
134
+ break;
135
+ case "stream.degraded":
136
+ this._setStatus("degraded");
137
+ this.callbacks.onDegraded?.(msg.payload.reason);
138
+ break;
139
+ case "stream.error":
140
+ this.callbacks.onError?.({
141
+ code: msg.payload.code,
142
+ message: msg.payload.message ?? msg.payload.code,
143
+ recoverable: msg.payload.recoverable
144
+ });
145
+ break;
146
+ }
147
+ }
148
+ _requestSnapshot() {
149
+ if (!this.lastSubscribe || !this.ws || this.ws.readyState !== WebSocket.OPEN) return;
150
+ this.ws.send(JSON.stringify({ type: "subscribe", ...this.lastSubscribe }));
151
+ }
152
+ _resetHeartbeatTimer() {
153
+ this._clearHeartbeatTimer();
154
+ this.heartbeatTimer = setInterval(() => {
155
+ if (Date.now() - this.lastHeartbeat > this.heartbeatTimeoutMs) {
156
+ this._setStatus("degraded");
157
+ this.callbacks.onDegraded?.("heartbeat_timeout");
158
+ }
159
+ }, this.heartbeatTimeoutMs / 2);
160
+ }
161
+ _clearHeartbeatTimer() {
162
+ if (this.heartbeatTimer) {
163
+ clearInterval(this.heartbeatTimer);
164
+ this.heartbeatTimer = null;
165
+ }
166
+ }
167
+ _scheduleReconnect() {
168
+ const delay = Math.min(1e3 * 2 ** this.attempt, this.maxReconnectDelay);
169
+ this.attempt++;
170
+ setTimeout(() => {
171
+ if (!this.stopped) this._connect();
172
+ }, delay);
173
+ }
174
+ };
175
+
176
+ // src/sdk/websocket/replay.ts
177
+ var toIso = (v) => v instanceof Date ? v.toISOString() : v;
178
+ var AeroReplayClient = class {
179
+ constructor(wsBase, token, wsPath = "/ws/tracks/replay") {
180
+ this.wsBase = wsBase;
181
+ this.token = token;
182
+ this.wsPath = wsPath;
183
+ }
184
+ wsBase;
185
+ token;
186
+ wsPath;
187
+ /** Open a replay stream. Returns a handle for transport control. */
188
+ stream(opts, callbacks) {
189
+ const params = new URLSearchParams({ token: this.token });
190
+ if (opts.airportId) params.set("airportId", opts.airportId);
191
+ if (opts.sessionId) params.set("sessionId", opts.sessionId);
192
+ const ws = new WebSocket(`${this.wsBase}${this.wsPath}?${params.toString()}`);
193
+ let sessionId = opts.sessionId ?? null;
194
+ let closedByUser = false;
195
+ ws.onopen = () => {
196
+ if (!opts.sessionId) {
197
+ ws.send(JSON.stringify({
198
+ type: "replay.start",
199
+ from: toIso(opts.from),
200
+ to: toIso(opts.to),
201
+ speed: opts.speed ?? 1
202
+ }));
203
+ }
204
+ };
205
+ ws.onmessage = (ev) => {
206
+ let msg;
207
+ try {
208
+ msg = JSON.parse(String(ev.data));
209
+ } catch {
210
+ return;
211
+ }
212
+ const payload = msg.payload ?? {};
213
+ switch (msg.type) {
214
+ case "snapshot": {
215
+ if (typeof payload["sessionId"] === "string") sessionId = payload["sessionId"];
216
+ callbacks.onSnapshot?.(payload["tracks"] ?? [], sessionId ?? "");
217
+ break;
218
+ }
219
+ case "track.updated":
220
+ callbacks.onTrackUpdate?.(payload["track"]);
221
+ break;
222
+ case "track.removed":
223
+ callbacks.onTrackRemoved?.({
224
+ trackId: String(payload["trackId"] ?? ""),
225
+ reason: String(payload["reason"] ?? "")
226
+ });
227
+ break;
228
+ case "replay.progress": {
229
+ const progress = payload;
230
+ sessionId = progress.sessionId ?? sessionId;
231
+ callbacks.onProgress?.(progress);
232
+ break;
233
+ }
234
+ case "replay.done":
235
+ callbacks.onDone?.(String(payload["sessionId"] ?? sessionId ?? ""));
236
+ break;
237
+ }
238
+ };
239
+ ws.onerror = () => {
240
+ if (!closedByUser) callbacks.onError?.(new Error("replay websocket error"));
241
+ };
242
+ ws.onclose = (ev) => {
243
+ if (!closedByUser && ev.code >= 4e3) {
244
+ callbacks.onError?.(new Error(`replay websocket closed: ${ev.reason || ev.code}`));
245
+ }
246
+ };
247
+ const send = (body) => {
248
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(body));
249
+ };
250
+ return {
251
+ get sessionId() {
252
+ return sessionId;
253
+ },
254
+ pause: () => send({ type: "replay.pause" }),
255
+ resume: () => send({ type: "replay.resume" }),
256
+ seek: (at) => send({ type: "replay.seek", at: toIso(at) }),
257
+ stop: () => {
258
+ closedByUser = true;
259
+ send({ type: "replay.stop" });
260
+ try {
261
+ ws.close();
262
+ } catch {
263
+ }
264
+ }
265
+ };
266
+ }
267
+ };
268
+
269
+ // src/sdk/api/trpc.ts
270
+ import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
271
+ function createAeroTrpcClient(endpoint, token) {
272
+ const base = endpoint.replace(/\/$/, "");
273
+ return createTRPCProxyClient({
274
+ links: [
275
+ httpBatchLink({
276
+ url: `${base}/trpc`,
277
+ headers: () => ({ "x-aero-token": token }),
278
+ methodOverride: "POST"
279
+ })
280
+ ]
281
+ });
282
+ }
283
+
284
+ // src/sdk/api/rest.ts
285
+ var AeroRestClient = class {
286
+ constructor(endpoint, token) {
287
+ this.endpoint = endpoint;
288
+ this.token = token;
289
+ }
290
+ endpoint;
291
+ token;
292
+ async req(method, path, body) {
293
+ const res = await fetch(`${this.endpoint}${path}`, {
294
+ method,
295
+ headers: {
296
+ "Content-Type": "application/json",
297
+ "x-aero-token": this.token
298
+ },
299
+ body: body ? JSON.stringify(body) : void 0
300
+ });
301
+ if (!res.ok) throw new Error(`${method} ${path} \u2192 ${res.status}`);
302
+ return res.json();
303
+ }
304
+ createSource(body) {
305
+ return this.req("POST", "/v1/sources", body);
306
+ }
307
+ listSources() {
308
+ return this.req("GET", "/v1/sources");
309
+ }
310
+ deleteSource(id) {
311
+ return this.req("DELETE", `/v1/sources/${id}`);
312
+ }
313
+ createWebhook(body) {
314
+ return this.req("POST", "/v1/webhooks", body);
315
+ }
316
+ getTracksCurrent(params) {
317
+ const search = new URLSearchParams();
318
+ if (params?.bbox) search.set("bbox", params.bbox.join(","));
319
+ if (params?.minConfidence != null) search.set("minConfidence", String(params.minConfidence));
320
+ const qs = search.size ? `?${search.toString()}` : "";
321
+ return this.req("GET", `/v1/tracks/current${qs}`);
322
+ }
323
+ health() {
324
+ return this.req("GET", "/health");
325
+ }
326
+ };
327
+
328
+ // src/sdk/api/graphql.ts
329
+ var AeroGraphQLClient = class {
330
+ constructor(endpoint, token) {
331
+ this.endpoint = endpoint;
332
+ this.token = token;
333
+ }
334
+ endpoint;
335
+ token;
336
+ async query(query, variables) {
337
+ const res = await fetch(`${this.endpoint}/graphql`, {
338
+ method: "POST",
339
+ headers: {
340
+ "Content-Type": "application/json",
341
+ "x-aero-token": this.token
342
+ },
343
+ body: JSON.stringify({ query, variables })
344
+ });
345
+ if (!res.ok) throw new Error(`GraphQL ${res.status}: ${await res.text().catch(() => "")}`);
346
+ const json = await res.json();
347
+ if (json.errors?.length) throw new Error(JSON.stringify(json.errors));
348
+ return json.data;
349
+ }
350
+ };
351
+
352
+ // src/sdk/cache/index.ts
353
+ import { isRenderableTrack } from "@aeroswarm/core/utils";
354
+ var TypedEmitter = class {
355
+ listeners = /* @__PURE__ */ new Map();
356
+ on(event, fn) {
357
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
358
+ this.listeners.get(event).add(fn);
359
+ return () => this.listeners.get(event)?.delete(fn);
360
+ }
361
+ emit(event, payload) {
362
+ this.listeners.get(event)?.forEach((fn) => fn(payload));
363
+ }
364
+ };
365
+ var TrackCache = class extends TypedEmitter {
366
+ tracks = /* @__PURE__ */ new Map();
367
+ applyUpdate(track) {
368
+ if (track.status === "lost") {
369
+ this.tracks.delete(track.trackId);
370
+ this.emit("removed", { trackId: track.trackId });
371
+ return;
372
+ }
373
+ const merged = { ...this.tracks.get(track.trackId), ...track };
374
+ this.tracks.set(track.trackId, merged);
375
+ this.emit("update", merged);
376
+ }
377
+ applyRemoved(ev) {
378
+ this.tracks.delete(ev.trackId);
379
+ this.emit("removed", ev);
380
+ }
381
+ applySnapshot(tracks) {
382
+ this.tracks.clear();
383
+ for (const t of tracks) {
384
+ if (isRenderableTrack(t)) this.tracks.set(t.trackId, t);
385
+ }
386
+ this.emit("snapshot", Array.from(this.tracks.values()));
387
+ }
388
+ getAll() {
389
+ return Array.from(this.tracks.values());
390
+ }
391
+ get(id) {
392
+ return this.tracks.get(id);
393
+ }
394
+ get size() {
395
+ return this.tracks.size;
396
+ }
397
+ clear() {
398
+ this.tracks.clear();
399
+ }
400
+ getRenderable() {
401
+ return this.getAll().filter(isRenderableTrack);
402
+ }
403
+ };
404
+
405
+ // src/sdk/index.ts
406
+ var AeroSwarm = class _AeroSwarm {
407
+ constructor(opts) {
408
+ this.opts = opts;
409
+ this.trpc = createAeroTrpcClient(opts.endpoint, opts.token);
410
+ this.rest = new AeroRestClient(opts.endpoint, opts.token);
411
+ this.graphql = new AeroGraphQLClient(opts.endpoint, opts.token);
412
+ this.cache = new TrackCache();
413
+ const wsBase = (opts.wsEndpoint ?? opts.endpoint).replace(/^http/, "ws");
414
+ this.wsClient = new AeroWsClient(
415
+ `${wsBase}/ws/tracks/live`,
416
+ opts.token,
417
+ opts.maxReconnectDelay ?? 3e4
418
+ );
419
+ }
420
+ opts;
421
+ trpc;
422
+ rest;
423
+ graphql;
424
+ cache;
425
+ wsClient;
426
+ _status = "idle";
427
+ _lastUpdateAt = null;
428
+ _error = null;
429
+ _sourceStatus = {};
430
+ // ── Federated history resolution ──────────────────────────────────────
431
+ // An airport can serve its own replay/history through a backend mounting
432
+ // @aeroswarm/replay-server instead of AeroSwarm's own (see
433
+ // docs/development/federation/). `airports.list()`'s `historyEndpoint`
434
+ // field drives this automatically; every federatable call also accepts
435
+ // an explicit `endpoint` to bypass the registry entirely ("connect
436
+ // manual"). Priority: explicit endpoint > registry > AeroSwarm itself.
437
+ airportsCache = null;
438
+ static AIRPORTS_CACHE_TTL_MS = 6e4;
439
+ /** Resolves which base URL (if any) should serve a federated call.
440
+ * Returns null to mean "use AeroSwarm's own endpoint" — the normal,
441
+ * unfederated path, which is also what a lookup failure falls back to
442
+ * (a broken airports.list() call must never brick replay/history for
443
+ * everyone else). */
444
+ async resolveHistoryEndpoint(airportId, override) {
445
+ if (override) return override.replace(/\/$/, "");
446
+ if (!airportId) return null;
447
+ try {
448
+ const now = Date.now();
449
+ if (!this.airportsCache || now - this.airportsCache.fetchedAt > _AeroSwarm.AIRPORTS_CACHE_TTL_MS) {
450
+ const { airports } = await this.airports.list();
451
+ this.airportsCache = {
452
+ byId: new Map(airports.map((a) => [a.id, a.historyEndpoint])),
453
+ fetchedAt: now
454
+ };
455
+ }
456
+ const endpoint = this.airportsCache.byId.get(airportId) ?? null;
457
+ return endpoint ? endpoint.replace(/\/$/, "") : null;
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
462
+ async federatedFetch(base, path, query) {
463
+ const url = new URL(`${base}${path}`);
464
+ for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
465
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${this.opts.token}` } });
466
+ if (!res.ok) throw new Error(`Federated request failed: ${res.status} ${res.statusText} (${url.pathname})`);
467
+ return res.json();
468
+ }
469
+ tracks = {
470
+ subscribe: (opts, callbacks) => {
471
+ this.wsClient.connect(
472
+ { encoding: this.opts.encoding ?? "json", ...opts },
473
+ {
474
+ ...callbacks,
475
+ onStatusChange: (s) => {
476
+ this._status = s;
477
+ callbacks.onStatusChange?.(s);
478
+ },
479
+ onSnapshot: (tracks) => {
480
+ this.cache.applySnapshot(tracks);
481
+ this._lastUpdateAt = Date.now();
482
+ callbacks.onSnapshot?.(tracks);
483
+ },
484
+ onTrackUpdate: (track) => {
485
+ this.cache.applyUpdate(track);
486
+ this._lastUpdateAt = Date.now();
487
+ callbacks.onTrackUpdate?.(track);
488
+ },
489
+ onTrackRemoved: (ev) => {
490
+ this.cache.applyRemoved(ev);
491
+ callbacks.onTrackRemoved?.(ev);
492
+ },
493
+ onSourceStatus: (ev) => {
494
+ this._sourceStatus[ev.sourceId] = ev.status;
495
+ callbacks.onSourceStatus?.(ev);
496
+ },
497
+ onError: (err) => {
498
+ this._error = err;
499
+ callbacks.onError?.(err);
500
+ }
501
+ }
502
+ );
503
+ return () => this.wsClient.disconnect();
504
+ },
505
+ updateFilter: (opts) => this.wsClient.updateFilter(opts),
506
+ stream: (opts) => {
507
+ const queue = [];
508
+ let resolve = null;
509
+ let done = false;
510
+ this.wsClient.connect({ encoding: "json", ...opts }, {
511
+ onTrackUpdate: (t) => {
512
+ if (resolve) {
513
+ resolve({ value: t, done: false });
514
+ resolve = null;
515
+ } else queue.push(t);
516
+ },
517
+ onStatusChange: (s) => {
518
+ if (s === "offline") done = true;
519
+ }
520
+ });
521
+ return {
522
+ [Symbol.asyncIterator]: () => ({
523
+ next: () => {
524
+ if (queue.length) return Promise.resolve({ value: queue.shift(), done: false });
525
+ if (done) return Promise.resolve({ value: void 0, done: true });
526
+ return new Promise((r) => {
527
+ resolve = r;
528
+ });
529
+ },
530
+ return: async () => {
531
+ this.wsClient.disconnect();
532
+ return { value: void 0, done: true };
533
+ }
534
+ })
535
+ };
536
+ },
537
+ snapshot: (input) => this.trpc.tracks.snapshot.query(input ?? {}),
538
+ /** Persisted position history for one track. If the airport has a
539
+ * `historyEndpoint` (or `endpoint` is passed explicitly), this calls
540
+ * that backend's `@aeroswarm/replay-server` instead of AeroSwarm's
541
+ * own local database — see docs/development/federation/003-*.md. */
542
+ history: async (input) => {
543
+ const base = await this.resolveHistoryEndpoint(input.airportId, input.endpoint);
544
+ if (base) {
545
+ return this.federatedFetch(base, `/tracks/${encodeURIComponent(input.trackId)}/history`, {
546
+ limit: input.limit?.toString()
547
+ });
548
+ }
549
+ return this.trpc.tracks.history.query(input);
550
+ }
551
+ };
552
+ sources = {
553
+ /** Registers a source and returns its token, tied to `airportId` — a
554
+ * source is a sensor physically at one airport, so this is required.
555
+ * Anything that later authenticates with the returned token (WS
556
+ * live/replay, tRPC calls) is scoped to that airport automatically,
557
+ * with no need to keep passing `airportId` on every subsequent call.
558
+ * For access spanning multiple airports, use the viewer/dashboard
559
+ * token instead — it's never auto-scoped. */
560
+ create: (body) => this.rest.createSource(body),
561
+ health: (sourceId) => this.trpc.sources.health.query({ sourceId }),
562
+ list: (input) => this.trpc.sources.list.query(input ?? {}),
563
+ /** Reassigning `airportId` (including to `null` to clear it) re-caches
564
+ * the source's token immediately, so the new scoping takes effect
565
+ * without waiting out the token cache's TTL. */
566
+ update: (input) => this.trpc.sources.update.mutate(input),
567
+ delete: (id) => this.trpc.sources.delete.mutate({ id })
568
+ };
569
+ /** Airport registry (multi-airport tenancy). */
570
+ airports = {
571
+ list: () => this.trpc.airports.list.query(),
572
+ create: (input) => this.trpc.airports.create.mutate(input),
573
+ update: (input) => this.trpc.airports.update.mutate(input),
574
+ delete: (input) => this.trpc.airports.delete.mutate(input),
575
+ /** Surface geometry (runway/taxiway/apron GeoJSON) for the map. */
576
+ surface: (input) => this.trpc.airports.surface.query(input),
577
+ setSurface: (input) => this.trpc.airports.setSurface.mutate(input)
578
+ };
579
+ replay = {
580
+ /** Data-availability calendar: which days have recorded tracks.
581
+ * Use this to populate/validate replay date pickers — days not in
582
+ * the list have no data. Federates to the airport's own backend when
583
+ * it has a `historyEndpoint` (or `endpoint` is passed explicitly) —
584
+ * see docs/development/federation/003-*.md. */
585
+ days: async (input) => {
586
+ const base = await this.resolveHistoryEndpoint(input?.airportId, input?.endpoint);
587
+ if (base) {
588
+ return this.federatedFetch(base, "/replay/days", {
589
+ lookbackDays: input?.lookbackDays?.toString()
590
+ });
591
+ }
592
+ return this.trpc.replay.days.query(input ?? {});
593
+ },
594
+ range: async (input) => {
595
+ const base = await this.resolveHistoryEndpoint(input.airportId, input.endpoint);
596
+ if (base) {
597
+ const summary = await this.federatedFetch(
598
+ base,
599
+ "/replay/range",
600
+ { from: input.from, to: input.to }
601
+ );
602
+ return { from: input.from, to: input.to, ...summary };
603
+ }
604
+ return this.trpc.replay.range.query(input);
605
+ },
606
+ // replay.window / replay.seek (bulk sample download for client-side
607
+ // interpolation) were removed — they were never part of the federation
608
+ // contract, and `replay.stream`'s server-paced WS (with server-side
609
+ // seek/re-snapshot) covers every use they had at a fraction of the
610
+ // payload. See docs/development/federation/005-*.md.
611
+ /** Server-paced WS playback: streams historical tracks at `speed`× in
612
+ * the same envelope format as the live stream. Returns a transport
613
+ * handle (pause/resume/seek/stop) — a Promise, since resolving which
614
+ * backend serves this airport (see docs/development/federation/003-*.md)
615
+ * is async. See `AeroReplayClient` for the raw wire protocol if you
616
+ * want to speak WS manually instead. */
617
+ stream: async (opts, callbacks) => {
618
+ const federatedBase = await this.resolveHistoryEndpoint(opts.airportId, opts.endpoint);
619
+ if (federatedBase) {
620
+ const wsBase2 = federatedBase.replace(/^http/, "ws");
621
+ const { airportId: _drop, ...rest } = opts;
622
+ return new AeroReplayClient(wsBase2, this.opts.token, "/replay").stream(rest, callbacks);
623
+ }
624
+ const wsBase = (this.opts.wsEndpoint ?? this.opts.endpoint).replace(/^http/, "ws");
625
+ return new AeroReplayClient(wsBase, this.opts.token).stream(opts, callbacks);
626
+ }
627
+ };
628
+ /** Movement analytics: takeoffs/landings per hour + peak hour, recorded
629
+ * by the pipeline's movement detector. */
630
+ analytics = {
631
+ movements: (input) => this.trpc.analytics.movements.query(input ?? {})
632
+ };
633
+ /** Route conformance: pre-defined ground routes + live track clearances.
634
+ * Assign a route to a track and the safety engine can detect when it
635
+ * strays beyond the route's corridor. */
636
+ routes = {
637
+ list: (input) => this.trpc.routes.list.query(input ?? {}),
638
+ create: (input) => this.trpc.routes.create.mutate(input),
639
+ delete: (input) => this.trpc.routes.delete.mutate(input),
640
+ assign: (input) => this.trpc.routes.assign.mutate(input),
641
+ unassign: (input) => this.trpc.routes.unassign.mutate(input),
642
+ assignments: () => this.trpc.routes.assignments.query()
643
+ };
644
+ webhooks = {
645
+ list: () => this.trpc.webhooks.list.query(),
646
+ create: (body) => this.trpc.webhooks.create.mutate(body),
647
+ delete: (input) => this.trpc.webhooks.delete.mutate(input)
648
+ };
649
+ system = {
650
+ stats: () => this.trpc.system.stats.query(),
651
+ infraHealth: () => this.trpc.system.infraHealth.query(),
652
+ ingestHistory: () => this.trpc.system.ingestHistory.query(),
653
+ getConfig: () => this.trpc.system.getConfig.query(),
654
+ setConfig: (input) => this.trpc.system.setConfig.mutate(input),
655
+ clearTable: (input) => this.trpc.system.clearTable.mutate(input)
656
+ };
657
+ getStatus() {
658
+ return this._status;
659
+ }
660
+ getLastUpdateAt() {
661
+ return this._lastUpdateAt;
662
+ }
663
+ getError() {
664
+ return this._error;
665
+ }
666
+ getSourceStatus() {
667
+ return { ...this._sourceStatus };
668
+ }
669
+ disconnect() {
670
+ this.wsClient.disconnect();
671
+ }
672
+ };
673
+
674
+ // src/react/context/index.tsx
675
+ import { jsx } from "react/jsx-runtime";
676
+ var AeroSwarmContext = createContext(null);
677
+ function useAeroSwarmContext() {
678
+ const ctx = useContext(AeroSwarmContext);
679
+ if (!ctx) throw new Error("useAeroSwarmContext must be used inside <AeroSwarmProvider>");
680
+ return ctx;
681
+ }
682
+ function AeroSwarmProvider({
683
+ children,
684
+ queryClient: externalQc,
685
+ ...sdkOpts
686
+ }) {
687
+ const client = useMemo(
688
+ () => new AeroSwarm(sdkOpts),
689
+ // eslint-disable-next-line react-hooks/exhaustive-deps
690
+ [sdkOpts.endpoint, sdkOpts.token]
691
+ );
692
+ const qc = useMemo(
693
+ () => externalQc ?? new QueryClient({
694
+ defaultOptions: {
695
+ queries: {
696
+ staleTime: 1e4,
697
+ refetchInterval: 3e4
698
+ }
699
+ }
700
+ }),
701
+ [externalQc]
702
+ );
703
+ return /* @__PURE__ */ jsx(QueryClientProvider, { client: qc, children: /* @__PURE__ */ jsx(AeroSwarmContext.Provider, { value: { client }, children }) });
704
+ }
705
+
706
+ // src/react/hooks/use-track-history.ts
707
+ import { useCallback, useEffect, useRef } from "react";
708
+ var DR_MIN_SPEED_KT = 10;
709
+ var MAX_SPEED_KT = 1200;
710
+ var GAP_RESET_MS = 3e4;
711
+ var DR_MAX_MS = 18e4;
712
+ function deadReckon(baseLat, baseLon, speedKt, headingDeg, elapsedMs) {
713
+ const elapsedSec = elapsedMs / 1e3;
714
+ const distanceNm = speedKt * elapsedSec / 3600;
715
+ const headingRad = headingDeg * (Math.PI / 180);
716
+ const dlat = distanceNm / 60 * Math.cos(headingRad);
717
+ const dlon = distanceNm / (60 * Math.cos(baseLat * (Math.PI / 180))) * Math.sin(headingRad);
718
+ return { lat: baseLat + dlat, lon: baseLon + dlon };
719
+ }
720
+ function useTrackHistory(options) {
721
+ const {
722
+ maxHistoryPoints = 500,
723
+ minDeltaDeg = 1e-4,
724
+ maxJumpDeg = 5,
725
+ onFrame
726
+ } = options;
727
+ const currentPositions = useRef(/* @__PURE__ */ new Map());
728
+ const lastActualPos = useRef(/* @__PURE__ */ new Map());
729
+ const history = useRef(/* @__PURE__ */ new Map());
730
+ const lastSeen = useRef(/* @__PURE__ */ new Map());
731
+ const lastUpdPerf = useRef(/* @__PURE__ */ new Map());
732
+ const interp = useRef(/* @__PURE__ */ new Map());
733
+ const velocity = useRef(/* @__PURE__ */ new Map());
734
+ const drState = useRef(/* @__PURE__ */ new Map());
735
+ const rafRef = useRef(null);
736
+ const onFrameRef = useRef(onFrame);
737
+ onFrameRef.current = onFrame;
738
+ const maxHistoryPointsRef = useRef(maxHistoryPoints);
739
+ maxHistoryPointsRef.current = maxHistoryPoints;
740
+ const minDeltaDegRef = useRef(minDeltaDeg);
741
+ minDeltaDegRef.current = minDeltaDeg;
742
+ const maxJumpDegRef = useRef(maxJumpDeg);
743
+ maxJumpDegRef.current = maxJumpDeg;
744
+ function emitFrame() {
745
+ onFrameRef.current({
746
+ currentPositions: currentPositions.current,
747
+ history: history.current,
748
+ lastSeen: lastSeen.current,
749
+ isMoving: /* @__PURE__ */ new Set([...interp.current.keys(), ...drState.current.keys()])
750
+ });
751
+ }
752
+ function needsRaf() {
753
+ if (interp.current.size > 0) return true;
754
+ for (const [id, dr] of drState.current) {
755
+ if (interp.current.has(id)) continue;
756
+ const vel = velocity.current.get(id);
757
+ if (vel && vel.speedKt >= DR_MIN_SPEED_KT) return true;
758
+ if (performance.now() - dr.baseTime > DR_MAX_MS) drState.current.delete(id);
759
+ }
760
+ return false;
761
+ }
762
+ function ensureRaf() {
763
+ if (rafRef.current !== null || !needsRaf()) return;
764
+ const tick = (perfNow) => {
765
+ for (const [id, tw] of interp.current) {
766
+ const p = Math.min((perfNow - tw.startedAt) / tw.durationMs, 1);
767
+ const ease = p < 1 ? p * (2 - p) : 1;
768
+ const lat = tw.fromLat + (tw.toLat - tw.fromLat) * ease;
769
+ const lon = tw.fromLon + (tw.toLon - tw.fromLon) * ease;
770
+ currentPositions.current.set(id, { lat, lon });
771
+ if (p >= 1) {
772
+ interp.current.delete(id);
773
+ const vel = velocity.current.get(id);
774
+ if (vel && vel.speedKt >= DR_MIN_SPEED_KT) {
775
+ drState.current.set(id, { baseLat: lat, baseLon: lon, baseTime: perfNow });
776
+ }
777
+ }
778
+ }
779
+ for (const [id, dr] of drState.current) {
780
+ if (interp.current.has(id)) continue;
781
+ const vel = velocity.current.get(id);
782
+ if (!vel || vel.speedKt < DR_MIN_SPEED_KT) continue;
783
+ const elapsed = perfNow - dr.baseTime;
784
+ if (elapsed > DR_MAX_MS) {
785
+ drState.current.delete(id);
786
+ continue;
787
+ }
788
+ currentPositions.current.set(id, deadReckon(
789
+ dr.baseLat,
790
+ dr.baseLon,
791
+ vel.speedKt,
792
+ vel.headingDeg,
793
+ elapsed
794
+ ));
795
+ }
796
+ emitFrame();
797
+ rafRef.current = needsRaf() ? requestAnimationFrame(tick) : null;
798
+ };
799
+ rafRef.current = requestAnimationFrame(tick);
800
+ }
801
+ const processUpdate = useCallback((t) => {
802
+ if (t.lat == null || t.lon == null) return;
803
+ if (t.lat < -90 || t.lat > 90 || t.lon < -180 || t.lon > 180) return;
804
+ const perfNow = performance.now();
805
+ const wallNow = Date.now();
806
+ const id = t.trackId;
807
+ const lon = t.lon;
808
+ const lat = t.lat;
809
+ lastSeen.current.set(id, wallNow);
810
+ if (t.groundSpeedKt != null && t.headingDeg != null) {
811
+ const spd = t.groundSpeedKt;
812
+ const hdg = t.headingDeg;
813
+ if (spd >= 0 && spd <= MAX_SPEED_KT) {
814
+ if (spd >= DR_MIN_SPEED_KT || !velocity.current.has(id)) {
815
+ velocity.current.set(id, { speedKt: spd, headingDeg: hdg });
816
+ }
817
+ }
818
+ }
819
+ const prevActual = lastActualPos.current.get(id);
820
+ const prevUpdPerf = lastUpdPerf.current.get(id);
821
+ if (prevActual && prevUpdPerf && perfNow > prevUpdPerf) {
822
+ const dtSec = (perfNow - prevUpdPerf) / 1e3;
823
+ const dlatDeg = lat - prevActual.lat;
824
+ const dlonDeg = lon - prevActual.lon;
825
+ const dlatNm = dlatDeg * 60;
826
+ const dlonNm = dlonDeg * 60 * Math.cos(lat * Math.PI / 180);
827
+ const distNm = Math.sqrt(dlatNm * dlatNm + dlonNm * dlonNm);
828
+ const derivedSpd = distNm / dtSec * 3600;
829
+ if (dtSec >= 0.25 && derivedSpd >= DR_MIN_SPEED_KT && derivedSpd <= MAX_SPEED_KT) {
830
+ const derivedHdg = (Math.atan2(dlonNm, dlatNm) * 180 / Math.PI + 360) % 360;
831
+ const existing = velocity.current.get(id);
832
+ if (!existing || existing.speedKt < DR_MIN_SPEED_KT) {
833
+ velocity.current.set(id, { speedKt: derivedSpd, headingDeg: derivedHdg });
834
+ }
835
+ }
836
+ }
837
+ const cur = currentPositions.current.get(id);
838
+ const lastActual = lastActualPos.current.get(id);
839
+ if (!cur) {
840
+ currentPositions.current.set(id, { lat, lon });
841
+ lastActualPos.current.set(id, { lat, lon });
842
+ history.current.set(id, []);
843
+ lastUpdPerf.current.set(id, perfNow);
844
+ const vel = velocity.current.get(id);
845
+ if (vel && vel.speedKt >= DR_MIN_SPEED_KT) {
846
+ drState.current.set(id, { baseLat: lat, baseLon: lon, baseTime: perfNow });
847
+ }
848
+ emitFrame();
849
+ ensureRaf();
850
+ return;
851
+ }
852
+ if (prevUpdPerf !== void 0 && perfNow - prevUpdPerf > GAP_RESET_MS) {
853
+ history.current.set(id, []);
854
+ currentPositions.current.set(id, { lat, lon });
855
+ lastActualPos.current.set(id, { lat, lon });
856
+ lastUpdPerf.current.set(id, perfNow);
857
+ interp.current.delete(id);
858
+ drState.current.delete(id);
859
+ emitFrame();
860
+ ensureRaf();
861
+ return;
862
+ }
863
+ const minD = minDeltaDegRef.current;
864
+ const maxJ = maxJumpDegRef.current;
865
+ const checkLat = lastActual?.lat ?? cur.lat;
866
+ const checkLon = lastActual?.lon ?? cur.lon;
867
+ if (Math.abs(checkLat - lat) < minD && Math.abs(checkLon - lon) < minD) return;
868
+ if (lastActual && (Math.abs(lat - lastActual.lat) > maxJ || Math.abs(lon - lastActual.lon) > maxJ)) return;
869
+ lastActualPos.current.set(id, { lat, lon });
870
+ const prevPerf = lastUpdPerf.current.get(id) ?? perfNow;
871
+ const interval = Math.max(100, Math.min(perfNow - prevPerf, 5e3));
872
+ lastUpdPerf.current.set(id, perfNow);
873
+ const maxHP = maxHistoryPointsRef.current;
874
+ const hist = history.current.get(id) ?? [];
875
+ const last = hist.at(-1);
876
+ const dupThreshold = minD * 0.5;
877
+ if (!last || Math.abs(last[0] - cur.lon) > dupThreshold || Math.abs(last[1] - cur.lat) > dupThreshold) {
878
+ hist.push([cur.lon, cur.lat]);
879
+ if (hist.length > maxHP) hist.shift();
880
+ history.current.set(id, hist);
881
+ }
882
+ drState.current.delete(id);
883
+ interp.current.set(id, {
884
+ fromLat: cur.lat,
885
+ fromLon: cur.lon,
886
+ toLat: lat,
887
+ toLon: lon,
888
+ startedAt: perfNow,
889
+ durationMs: Math.min(interval * 0.95, 3e3)
890
+ });
891
+ emitFrame();
892
+ ensureRaf();
893
+ }, []);
894
+ const processRemove = useCallback((trackId) => {
895
+ currentPositions.current.delete(trackId);
896
+ lastActualPos.current.delete(trackId);
897
+ history.current.delete(trackId);
898
+ lastSeen.current.delete(trackId);
899
+ lastUpdPerf.current.delete(trackId);
900
+ interp.current.delete(trackId);
901
+ velocity.current.delete(trackId);
902
+ drState.current.delete(trackId);
903
+ emitFrame();
904
+ }, []);
905
+ useEffect(() => {
906
+ return () => {
907
+ if (rafRef.current !== null) {
908
+ cancelAnimationFrame(rafRef.current);
909
+ rafRef.current = null;
910
+ }
911
+ };
912
+ }, []);
913
+ const reemit = useCallback(() => {
914
+ emitFrame();
915
+ }, []);
916
+ return { processUpdate, processRemove, reemit };
917
+ }
918
+
919
+ // src/react/hooks/index.ts
920
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
921
+ import { useQuery } from "@tanstack/react-query";
922
+ import { useShallow } from "zustand/react/shallow";
923
+
924
+ // src/react/store/index.ts
925
+ import { create } from "zustand";
926
+
927
+ // src/shared/utils/index.ts
928
+ import { isRenderableTrack as isRenderableTrack2, haversineKm, isInsideBbox, toCurrentTrack, clamp } from "@aeroswarm/core/utils";
929
+
930
+ // src/react/store/index.ts
931
+ var useTrackStore = create()((set, get) => ({
932
+ tracks: /* @__PURE__ */ new Map(),
933
+ status: "idle",
934
+ sourceStatus: {},
935
+ lastUpdateAt: null,
936
+ error: null,
937
+ isDegraded: false,
938
+ degradedReason: null,
939
+ applyUpdate(track) {
940
+ const next = new Map(get().tracks);
941
+ next.set(track.trackId, { ...next.get(track.trackId), ...track });
942
+ set({ tracks: next, lastUpdateAt: Date.now() });
943
+ },
944
+ applyBatch(updates, removedIds) {
945
+ if (updates.length === 0 && removedIds.length === 0) return;
946
+ const next = new Map(get().tracks);
947
+ for (const track of updates) {
948
+ next.set(track.trackId, { ...next.get(track.trackId), ...track });
949
+ }
950
+ for (const id of removedIds) next.delete(id);
951
+ set({ tracks: next, lastUpdateAt: Date.now() });
952
+ },
953
+ applyRemoved({ trackId }) {
954
+ const next = new Map(get().tracks);
955
+ next.delete(trackId);
956
+ set({ tracks: next });
957
+ },
958
+ applySnapshot(tracks) {
959
+ const next = /* @__PURE__ */ new Map();
960
+ for (const t of tracks) {
961
+ if (isRenderableTrack2(t)) next.set(t.trackId, t);
962
+ }
963
+ set({ tracks: next, lastUpdateAt: Date.now() });
964
+ },
965
+ setStatus(s) {
966
+ set({
967
+ status: s,
968
+ isDegraded: s === "degraded",
969
+ degradedReason: s === "connected" ? null : get().degradedReason
970
+ });
971
+ },
972
+ setSourceStatus(sourceId, status) {
973
+ set({ sourceStatus: { ...get().sourceStatus, [sourceId]: status } });
974
+ },
975
+ setError(err) {
976
+ set({ error: err });
977
+ },
978
+ setDegraded(reason) {
979
+ set({
980
+ isDegraded: reason !== null,
981
+ degradedReason: reason,
982
+ status: reason !== null ? "degraded" : get().status
983
+ });
984
+ },
985
+ pruneStale(maxAgeMs) {
986
+ const cutoff = Date.now() - maxAgeMs;
987
+ const removed = [];
988
+ const next = new Map(get().tracks);
989
+ for (const [id, t] of next) {
990
+ if ((t.lastSeenAt ?? 0) < cutoff) {
991
+ next.delete(id);
992
+ removed.push(id);
993
+ }
994
+ }
995
+ if (removed.length > 0) set({ tracks: next });
996
+ return removed;
997
+ },
998
+ clear() {
999
+ set({ tracks: /* @__PURE__ */ new Map(), lastUpdateAt: null });
1000
+ }
1001
+ }));
1002
+ var selectAllTracks = (s) => Array.from(s.tracks.values());
1003
+ var selectTrack = (id) => (s) => s.tracks.get(id);
1004
+ var selectTrackCount = (s) => s.tracks.size;
1005
+ var selectStatus = (s) => s.status;
1006
+ var selectIsDegraded = (s) => s.isDegraded;
1007
+ var selectLastUpdateAt = (s) => s.lastUpdateAt;
1008
+ var selectError = (s) => s.error;
1009
+ var selectRenderableTracks = (s) => Array.from(s.tracks.values()).filter(isRenderableTrack2);
1010
+
1011
+ // src/react/hooks/index.ts
1012
+ function useAeroTracks(opts = {}) {
1013
+ const { client } = useAeroSwarmContext();
1014
+ const tracks = useTrackStore(useShallow(selectRenderableTracks));
1015
+ const allTracks = useTrackStore(useShallow(selectAllTracks));
1016
+ const status = useTrackStore(selectStatus);
1017
+ const isDegraded = useTrackStore(selectIsDegraded);
1018
+ const lastUpdateAt = useTrackStore(selectLastUpdateAt);
1019
+ const error = useTrackStore(selectError);
1020
+ const unsubRef = useRef2(null);
1021
+ const { isLoading } = useQuery({
1022
+ queryKey: ["aero-snapshot", opts.bbox?.join(","), opts.source, opts.minConfidence],
1023
+ enabled: opts.enabled !== false,
1024
+ staleTime: 5e3,
1025
+ queryFn: async () => {
1026
+ const result = await client.tracks.snapshot({
1027
+ bbox: opts.bbox,
1028
+ minConfidence: opts.minConfidence
1029
+ });
1030
+ useTrackStore.getState().applySnapshot(result.tracks ?? []);
1031
+ return result;
1032
+ }
1033
+ });
1034
+ useEffect2(() => {
1035
+ if (opts.enabled === false) return;
1036
+ const actions = useTrackStore.getState();
1037
+ unsubRef.current = client.tracks.subscribe(
1038
+ {
1039
+ bbox: opts.bbox,
1040
+ source: opts.source ?? "fused",
1041
+ minConfidence: opts.minConfidence,
1042
+ maxUpdateHz: opts.maxUpdateHz ?? 20,
1043
+ mode: "delta"
1044
+ },
1045
+ {
1046
+ onStatusChange: (s) => actions.setStatus(s),
1047
+ onSnapshot: (t) => actions.applySnapshot(t),
1048
+ onTrackUpdate: (t) => actions.applyUpdate(t),
1049
+ onTrackRemoved: (ev) => actions.applyRemoved(ev),
1050
+ onSourceStatus: (ev) => actions.setSourceStatus(ev.sourceId, ev.status),
1051
+ onDegraded: (r) => actions.setDegraded(r),
1052
+ onError: (e) => actions.setError(e)
1053
+ }
1054
+ );
1055
+ return () => {
1056
+ unsubRef.current?.();
1057
+ useTrackStore.getState().setStatus("idle");
1058
+ };
1059
+ }, [opts.bbox?.join(","), opts.source, opts.minConfidence, opts.maxUpdateHz, opts.enabled]);
1060
+ return {
1061
+ tracks,
1062
+ allTracks,
1063
+ status,
1064
+ isConnected: status === "connected",
1065
+ isLoading,
1066
+ isDegraded,
1067
+ lastUpdateAt,
1068
+ error,
1069
+ trackCount: tracks.length
1070
+ };
1071
+ }
1072
+ var useTracks = useAeroTracks;
1073
+ function useAeroTrack(trackId) {
1074
+ const track = useTrackStore(useMemo2(() => selectTrack(trackId), [trackId]));
1075
+ return { track };
1076
+ }
1077
+ function useAeroSnapshot(opts = {}) {
1078
+ const { client } = useAeroSwarmContext();
1079
+ return useQuery({
1080
+ queryKey: ["aero-snapshot-only", opts.bbox?.join(","), opts.source],
1081
+ enabled: opts.enabled !== false,
1082
+ queryFn: () => client.tracks.snapshot({ bbox: opts.bbox, minConfidence: opts.minConfidence })
1083
+ });
1084
+ }
1085
+ function useSourceHealth(sourceId) {
1086
+ const { client } = useAeroSwarmContext();
1087
+ return useQuery({
1088
+ queryKey: ["aero-source-health", sourceId],
1089
+ refetchInterval: 15e3,
1090
+ queryFn: () => sourceId ? client.sources.health(sourceId) : client.sources.list()
1091
+ });
1092
+ }
1093
+ function useTrackCount() {
1094
+ return useTrackStore(selectTrackCount);
1095
+ }
1096
+ function useAeroStatus() {
1097
+ return {
1098
+ status: useTrackStore(selectStatus),
1099
+ isDegraded: useTrackStore(selectIsDegraded),
1100
+ lastUpdateAt: useTrackStore(selectLastUpdateAt),
1101
+ error: useTrackStore(selectError)
1102
+ };
1103
+ }
1104
+ function useReplayStatus(sessionId, opts = {}) {
1105
+ const { client } = useAeroSwarmContext();
1106
+ return useQuery({
1107
+ queryKey: ["aero-replay-status", sessionId],
1108
+ enabled: opts.enabled !== false,
1109
+ refetchInterval: opts.refetchInterval ?? 1e3,
1110
+ queryFn: () => client.replay?.status?.query?.({ sessionId }) ?? Promise.resolve({ status: "idle" })
1111
+ });
1112
+ }
1113
+ function useReplayStop() {
1114
+ const { client } = useAeroSwarmContext();
1115
+ return useQuery({
1116
+ queryKey: ["aero-replay-stop"],
1117
+ enabled: false,
1118
+ queryFn: () => client.replay?.stop?.query?.({ sessionId: "" }) ?? Promise.resolve({ success: true })
1119
+ });
1120
+ }
1121
+ function useWebhooks(opts = {}) {
1122
+ const { client } = useAeroSwarmContext();
1123
+ return useQuery({
1124
+ queryKey: ["aero-webhooks"],
1125
+ enabled: opts.enabled !== false,
1126
+ queryFn: () => client.webhooks?.list?.query?.() ?? Promise.resolve({ webhooks: [] })
1127
+ });
1128
+ }
1129
+ function useLineage(eventId, opts = {}) {
1130
+ const { client } = useAeroSwarmContext();
1131
+ return useQuery({
1132
+ queryKey: ["aero-lineage", eventId],
1133
+ enabled: opts.enabled !== false && !!eventId,
1134
+ queryFn: () => client.lineage?.get?.query?.({ eventId }) ?? Promise.resolve({ lineage: null })
1135
+ });
1136
+ }
1137
+ function useLineageTrace(trackId, opts = {}) {
1138
+ const { client } = useAeroSwarmContext();
1139
+ return useQuery({
1140
+ queryKey: ["aero-lineage-trace", trackId],
1141
+ enabled: opts.enabled !== false && !!trackId,
1142
+ queryFn: () => client.lineage?.trace?.query?.({ trackId }) ?? Promise.resolve({ trace: [] })
1143
+ });
1144
+ }
1145
+ export {
1146
+ AeroSwarmProvider,
1147
+ selectAllTracks,
1148
+ selectError,
1149
+ selectIsDegraded,
1150
+ selectLastUpdateAt,
1151
+ selectRenderableTracks,
1152
+ selectStatus,
1153
+ selectTrack,
1154
+ selectTrackCount,
1155
+ useAeroSnapshot,
1156
+ useAeroStatus,
1157
+ useAeroSwarmContext,
1158
+ useAeroTrack,
1159
+ useAeroTracks,
1160
+ useLineage,
1161
+ useLineageTrace,
1162
+ useReplayStatus,
1163
+ useReplayStop,
1164
+ useSourceHealth,
1165
+ useTrackCount,
1166
+ useTrackHistory,
1167
+ useTrackStore,
1168
+ useTracks,
1169
+ useWebhooks
1170
+ };
1171
+ //# sourceMappingURL=index.js.map