Fix online board live refresh parity
This commit is contained in:
@@ -385,7 +385,7 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
flights: `${flightId.carrier}${flightId.flightNumber}${flightId.suffix ?? ""}`,
|
||||
dates: `${flightId.date.slice(0, 4)}-${flightId.date.slice(4, 6)}-${flightId.date.slice(6, 8)}T00:00:00`,
|
||||
};
|
||||
const { flight: firstFlight, allFlights, daysOfFlight, loading, error } = useFlightDetails(detailsParams);
|
||||
const { flight: firstFlight, allFlights, daysOfFlight, loading, error, refresh } = useFlightDetails(detailsParams);
|
||||
|
||||
// Pick the flight matching the URL's flightId (date-based match). The API
|
||||
// response may contain multiple flights with the same flight number on
|
||||
@@ -395,8 +395,9 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
|
||||
// Live updates via SignalR
|
||||
const { flight: liveFlight, connectionStatus } = useLiveFlightDetails(
|
||||
flightId,
|
||||
flight?.flightId ?? flightId,
|
||||
flight,
|
||||
refresh,
|
||||
);
|
||||
|
||||
const displayFlight = connectionStatus === "live" && liveFlight ? liveFlight : flight;
|
||||
|
||||
@@ -375,6 +375,7 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
||||
const { flights: liveFlights, connectionStatus } = useLiveBoardSearch(
|
||||
liveBoardParams,
|
||||
flights,
|
||||
refresh,
|
||||
);
|
||||
|
||||
// Calendar days
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useCallback, useState, useEffect, useRef } from "react";
|
||||
import { useApiClient } from "@/shared/api/provider.js";
|
||||
import { getFlightDetails } from "../api.js";
|
||||
import type { FlightDetailsParams } from "../api.js";
|
||||
@@ -21,6 +21,7 @@ export interface UseFlightDetailsResult {
|
||||
daysOfFlight: string[];
|
||||
loading: boolean;
|
||||
error: ApiError | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,10 +36,15 @@ export function useFlightDetails(
|
||||
const [daysOfFlight, setDaysOfFlight] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<ApiError | null>(null);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
const paramsRef = useRef(params);
|
||||
paramsRef.current = params;
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setRefreshKey((k) => k + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
@@ -62,7 +68,7 @@ export function useFlightDetails(
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [client, params.flights, params.dates]);
|
||||
}, [client, params.flights, params.dates, refreshKey]);
|
||||
|
||||
return {
|
||||
flight: allFlights[0] ?? null,
|
||||
@@ -70,5 +76,6 @@ export function useFlightDetails(
|
||||
daysOfFlight,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ function createMockHub() {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
invoke: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn((method: string, handler: (...args: unknown[]) => void) => {
|
||||
const list = handlers[method] ?? [];
|
||||
handlers[method] = list;
|
||||
@@ -179,7 +180,7 @@ describe("useLiveBoardSearch", () => {
|
||||
expect(result.current.flights).toEqual(initial);
|
||||
});
|
||||
|
||||
it("subscribes to the correct channel", async () => {
|
||||
it("subscribes to Angular TrackerHub refresh and invokes SubscribeDate", async () => {
|
||||
const { hub } = installMockHub(HUB_URL);
|
||||
const params: LiveBoardSearchParams = {
|
||||
date: "20250115",
|
||||
@@ -193,17 +194,19 @@ describe("useLiveBoardSearch", () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(hub.on).toHaveBeenCalledWith(
|
||||
"board:20250115:SVO:LED",
|
||||
expect.any(Function),
|
||||
expect(hub.on).toHaveBeenCalledWith("RefreshDate", expect.any(Function));
|
||||
expect(hub.invoke).toHaveBeenCalledWith(
|
||||
"SubscribeDate",
|
||||
"20250115",
|
||||
"SVO",
|
||||
"LED",
|
||||
);
|
||||
});
|
||||
|
||||
it("updates flights when a SignalR message arrives", async () => {
|
||||
it("updates flights when a legacy array message arrives", async () => {
|
||||
const { hub } = installMockHub(HUB_URL);
|
||||
const params: LiveBoardSearchParams = { date: "20250115" };
|
||||
const initial = [makeFlight("f1")];
|
||||
const channel = "board:20250115::";
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLiveBoardSearch(params, initial),
|
||||
@@ -215,12 +218,30 @@ describe("useLiveBoardSearch", () => {
|
||||
|
||||
const updated = [makeFlight("f2")];
|
||||
act(() => {
|
||||
hub._simulateMessage(channel, updated);
|
||||
hub._simulateMessage("RefreshDate", updated);
|
||||
});
|
||||
|
||||
expect(result.current.flights).toEqual(updated);
|
||||
});
|
||||
|
||||
it("refreshes API data when TrackerHub sends RefreshDate", async () => {
|
||||
const { hub } = installMockHub(HUB_URL);
|
||||
const params: LiveBoardSearchParams = { date: "20250115" };
|
||||
const onRefresh = vi.fn();
|
||||
|
||||
renderHook(() => useLiveBoardSearch(params, [], onRefresh));
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
hub._simulateMessage("RefreshDate", "20250115");
|
||||
});
|
||||
|
||||
expect(onRefresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns idle connectionStatus during SSR", () => {
|
||||
const origWindow = globalThis.window;
|
||||
// @ts-expect-error -- intentionally deleting window for SSR simulation
|
||||
|
||||
@@ -49,13 +49,24 @@ export function buildBoardChannelKey(params: LiveBoardSearchParams): string {
|
||||
export function useLiveBoardSearch(
|
||||
params: LiveBoardSearchParams,
|
||||
initialFlights: ISimpleFlight[],
|
||||
onRefresh?: () => void,
|
||||
): UseLiveBoardSearchResult {
|
||||
const config = useMemo<UseLiveFlightsConfig<LiveBoardSearchParams>>(
|
||||
() => ({
|
||||
hubUrl: getEnv().SIGNALR_HUB_URL,
|
||||
channelKey: buildBoardChannelKey,
|
||||
subscription: {
|
||||
eventName: "RefreshDate",
|
||||
invokeMethodName: "SubscribeDate",
|
||||
args: (p) => [p.date, p.departure, p.arrival],
|
||||
},
|
||||
onMessage: (message) => {
|
||||
if (Array.isArray(message)) return message;
|
||||
onRefresh?.();
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[onRefresh],
|
||||
);
|
||||
|
||||
const { data, connectionStatus } = useLiveFlights<
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@/shared/signalr/connection.js";
|
||||
import {
|
||||
buildFlightChannelKey,
|
||||
buildTrackerFlightSubscriptionKey,
|
||||
useLiveFlightDetails,
|
||||
} from "./useLiveFlightDetails.js";
|
||||
import type { ISimpleFlight, IFlightId, IFlightLeg, IParsedFlightId } from "../types.js";
|
||||
@@ -32,6 +33,7 @@ function createMockHub() {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
invoke: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn((method: string, handler: (...args: unknown[]) => void) => {
|
||||
const list = handlers[method] ?? [];
|
||||
handlers[method] = list;
|
||||
@@ -160,6 +162,33 @@ describe("buildFlightChannelKey", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTrackerFlightSubscriptionKey", () => {
|
||||
it("uses compact URL date when dateLT is unavailable", () => {
|
||||
const id: IParsedFlightId = {
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
suffix: "A",
|
||||
date: "20250115",
|
||||
};
|
||||
|
||||
expect(buildTrackerFlightSubscriptionKey(id)).toBe("SU100A@20250115");
|
||||
});
|
||||
|
||||
it("uses API dateLT as-is when available", () => {
|
||||
const id: IFlightId = {
|
||||
carrier: "SU",
|
||||
flightNumber: "0100",
|
||||
suffix: "",
|
||||
date: "2025-01-15",
|
||||
dateLT: "2025-01-15T00:00:00",
|
||||
};
|
||||
|
||||
expect(buildTrackerFlightSubscriptionKey(id)).toBe(
|
||||
"SU0100@2025-01-15T00:00:00",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("useLiveFlightDetails", () => {
|
||||
const HUB_URL = "https://hub.test/tracker";
|
||||
|
||||
@@ -211,13 +240,13 @@ describe("useLiveFlightDetails", () => {
|
||||
expect(result.current.flight).toBeNull();
|
||||
});
|
||||
|
||||
it("subscribes to the correct channel", async () => {
|
||||
it("subscribes to Angular TrackerHub refresh and invokes Subscribe", async () => {
|
||||
const { hub } = installMockHub(HUB_URL);
|
||||
const id: IParsedFlightId = {
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
suffix: "A",
|
||||
date: "2025-01-15",
|
||||
date: "20250115",
|
||||
};
|
||||
|
||||
renderHook(() => useLiveFlightDetails(id, null));
|
||||
@@ -226,20 +255,17 @@ describe("useLiveFlightDetails", () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(hub.on).toHaveBeenCalledWith(
|
||||
"flight:SU100A@2025-01-15",
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(hub.on).toHaveBeenCalledWith("Refresh", expect.any(Function));
|
||||
expect(hub.invoke).toHaveBeenCalledWith("Subscribe", "SU100A@20250115");
|
||||
});
|
||||
|
||||
it("updates flight when a SignalR message arrives", async () => {
|
||||
it("updates flight when a legacy array message arrives", async () => {
|
||||
const { hub } = installMockHub(HUB_URL);
|
||||
const id: IParsedFlightId = {
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
date: "2025-01-15",
|
||||
};
|
||||
const channel = "flight:SU100@2025-01-15";
|
||||
const initial = makeFlight("f1");
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
@@ -252,13 +278,35 @@ describe("useLiveFlightDetails", () => {
|
||||
|
||||
const updated = [makeFlight("f2")];
|
||||
act(() => {
|
||||
hub._simulateMessage(channel, updated);
|
||||
hub._simulateMessage("Refresh", updated);
|
||||
});
|
||||
|
||||
// useLiveFlights replaces the full array; our hook takes [0]
|
||||
expect(result.current.flight).toEqual(updated[0]);
|
||||
});
|
||||
|
||||
it("refreshes API data when TrackerHub sends Refresh", async () => {
|
||||
const { hub } = installMockHub(HUB_URL);
|
||||
const id: IParsedFlightId = {
|
||||
carrier: "SU",
|
||||
flightNumber: "100",
|
||||
date: "20250115",
|
||||
};
|
||||
const onRefresh = vi.fn();
|
||||
|
||||
renderHook(() => useLiveFlightDetails(id, null, onRefresh));
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
hub._simulateMessage("Refresh", "SU0100@20250115");
|
||||
});
|
||||
|
||||
expect(onRefresh).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns idle connectionStatus during SSR", () => {
|
||||
const origWindow = globalThis.window;
|
||||
// @ts-expect-error -- intentionally deleting window for SSR simulation
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type UseLiveFlightsConfig,
|
||||
} from "@/shared/hooks/useLiveFlights.js";
|
||||
import type { ConnectionStatus } from "@/shared/signalr/connection.js";
|
||||
import type { ISimpleFlight, IParsedFlightId } from "../types.js";
|
||||
import type { ISimpleFlight, IFlightId, IParsedFlightId } from "../types.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -32,24 +32,42 @@ export interface UseLiveFlightDetailsResult {
|
||||
// Channel key builder (exported for testing)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function buildFlightChannelKey(id: IParsedFlightId): string {
|
||||
type LiveFlightId = IParsedFlightId | IFlightId;
|
||||
|
||||
export function buildFlightChannelKey(id: LiveFlightId): string {
|
||||
return `flight:${id.carrier}${id.flightNumber}${id.suffix ?? ""}@${id.date}`;
|
||||
}
|
||||
|
||||
export function buildTrackerFlightSubscriptionKey(id: LiveFlightId): string {
|
||||
const date = "dateLT" in id && id.dateLT ? id.dateLT : id.date.replace(/-/g, "");
|
||||
return `${id.carrier}${id.flightNumber}${id.suffix ?? ""}@${date}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useLiveFlightDetails(
|
||||
id: IParsedFlightId,
|
||||
id: LiveFlightId,
|
||||
initialFlight: ISimpleFlight | null,
|
||||
onRefresh?: () => void,
|
||||
): UseLiveFlightDetailsResult {
|
||||
const config = useMemo<UseLiveFlightsConfig<IParsedFlightId>>(
|
||||
const config = useMemo<UseLiveFlightsConfig<LiveFlightId>>(
|
||||
() => ({
|
||||
hubUrl: getEnv().SIGNALR_HUB_URL,
|
||||
channelKey: buildFlightChannelKey,
|
||||
subscription: {
|
||||
eventName: "Refresh",
|
||||
invokeMethodName: "Subscribe",
|
||||
args: (flightId) => [buildTrackerFlightSubscriptionKey(flightId)],
|
||||
},
|
||||
onMessage: (message) => {
|
||||
if (Array.isArray(message)) return message;
|
||||
onRefresh?.();
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[onRefresh],
|
||||
);
|
||||
|
||||
// useLiveFlights expects an array — wrap/unwrap the single flight
|
||||
@@ -59,7 +77,7 @@ export function useLiveFlightDetails(
|
||||
);
|
||||
|
||||
const { data, connectionStatus } = useLiveFlights<
|
||||
IParsedFlightId,
|
||||
LiveFlightId,
|
||||
ISimpleFlight
|
||||
>(id, initialData, config);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ function createMockHub() {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
invoke: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn((method: string, handler: (...args: unknown[]) => void) => {
|
||||
const list = handlers[method] ?? [];
|
||||
handlers[method] = list;
|
||||
@@ -110,6 +111,37 @@ describe("useLiveFlights", () => {
|
||||
expect(result.current.data).toEqual(newData);
|
||||
});
|
||||
|
||||
it("invokes subscription method and lets onMessage trigger a refresh", async () => {
|
||||
const { hub } = installMockHub(defaultConfig.hubUrl);
|
||||
const onMessage = vi.fn();
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
subscription: {
|
||||
eventName: "RefreshDate",
|
||||
invokeMethodName: "SubscribeDate",
|
||||
args: (p: { route: string }) => [p.route],
|
||||
},
|
||||
onMessage,
|
||||
};
|
||||
|
||||
renderHook(() =>
|
||||
useLiveFlights({ route: "SVO-LED" }, [], config),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(hub.on).toHaveBeenCalledWith("RefreshDate", expect.any(Function));
|
||||
expect(hub.invoke).toHaveBeenCalledWith("SubscribeDate", "SVO-LED");
|
||||
|
||||
act(() => {
|
||||
hub._simulateMessage("RefreshDate", "20260506");
|
||||
});
|
||||
|
||||
expect(onMessage).toHaveBeenCalledWith("20260506");
|
||||
});
|
||||
|
||||
it("cleans up subscription on unmount", async () => {
|
||||
const { hub } = installMockHub(defaultConfig.hubUrl);
|
||||
|
||||
|
||||
@@ -4,9 +4,17 @@ import {
|
||||
type ConnectionStatus,
|
||||
} from "../signalr/connection.js";
|
||||
|
||||
export interface LiveSubscription<TParams> {
|
||||
eventName: string;
|
||||
invokeMethodName: string;
|
||||
args: (params: TParams) => unknown[];
|
||||
}
|
||||
|
||||
export interface UseLiveFlightsConfig<TParams> {
|
||||
hubUrl: string;
|
||||
channelKey: (params: TParams) => string;
|
||||
subscription?: LiveSubscription<TParams>;
|
||||
onMessage?: (message: unknown) => void | unknown[];
|
||||
}
|
||||
|
||||
export interface UseLiveFlightsResult<TData> {
|
||||
@@ -26,15 +34,18 @@ export function useLiveFlights<TParams, TData>(
|
||||
initialData: TData[],
|
||||
config: UseLiveFlightsConfig<TParams>,
|
||||
): UseLiveFlightsResult<TData> {
|
||||
const [data, setData] = useState<TData[]>(initialData);
|
||||
const [data, setData] = useState<TData[] | null>(null);
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("idle");
|
||||
|
||||
// Keep a stable reference to initialData for the SSR short-circuit
|
||||
const initialDataRef = useRef(initialData);
|
||||
initialDataRef.current = initialData;
|
||||
const onMessageRef = useRef(config.onMessage);
|
||||
onMessageRef.current = config.onMessage;
|
||||
|
||||
const isClient = typeof window !== "undefined";
|
||||
const paramsKey = config.channelKey(params);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
@@ -43,8 +54,9 @@ export function useLiveFlights<TParams, TData>(
|
||||
// real SIGNALR_HUB_URL (same-origin or CORS-enabled).
|
||||
if (!config.hubUrl) return;
|
||||
|
||||
const channel = config.channelKey(params);
|
||||
const channel = config.subscription?.eventName ?? paramsKey;
|
||||
const connection = getSharedConnection({ hubUrl: config.hubUrl });
|
||||
setData(null);
|
||||
|
||||
// Sync current status
|
||||
setConnectionStatus(connection.status);
|
||||
@@ -54,18 +66,33 @@ export function useLiveFlights<TParams, TData>(
|
||||
});
|
||||
|
||||
const unsubChannel = connection.subscribe(channel, (message) => {
|
||||
const nextData = onMessageRef.current?.(message);
|
||||
if (Array.isArray(nextData)) {
|
||||
setData(nextData as TData[]);
|
||||
return;
|
||||
}
|
||||
if (!onMessageRef.current) {
|
||||
setData(message as TData[]);
|
||||
}
|
||||
});
|
||||
|
||||
const subscription = config.subscription;
|
||||
if (subscription) {
|
||||
void connection.invoke(
|
||||
subscription.invokeMethodName,
|
||||
...subscription.args(params),
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubChannel();
|
||||
unsubStatus();
|
||||
};
|
||||
}, [isClient, config.hubUrl, config.channelKey, params]);
|
||||
}, [isClient, config.hubUrl, config.subscription, paramsKey]);
|
||||
|
||||
if (!isClient) {
|
||||
return { data: initialData, connectionStatus: "idle" };
|
||||
}
|
||||
|
||||
return { data, connectionStatus };
|
||||
return { data: data ?? initialData, connectionStatus };
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ function createMockHub() {
|
||||
const hub = {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
invoke: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn((method: string, handler: (...args: unknown[]) => void) => {
|
||||
const list = handlers[method] ?? [];
|
||||
handlers[method] = list;
|
||||
@@ -233,6 +234,22 @@ describe("SignalRConnection", () => {
|
||||
expect(hub.stop).toHaveBeenCalledTimes(1);
|
||||
expect(conn.status).toBe("idle");
|
||||
});
|
||||
|
||||
it("invokes hub methods after the lazy connection starts", async () => {
|
||||
const hub = createMockHub();
|
||||
const conn = createConnection(hub);
|
||||
|
||||
await conn.invoke("SubscribeDate", "20260506", "MOW", "LED");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(hub.start).toHaveBeenCalledTimes(1);
|
||||
expect(hub.invoke).toHaveBeenCalledWith(
|
||||
"SubscribeDate",
|
||||
"20260506",
|
||||
"MOW",
|
||||
"LED",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSharedConnection", () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ type MessageHandler = (message: unknown) => void;
|
||||
interface HubConnectionLike {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
invoke(methodName: string, ...args: unknown[]): Promise<unknown>;
|
||||
on(methodName: string, handler: (...args: unknown[]) => void): void;
|
||||
off(methodName: string, handler: (...args: unknown[]) => void): void;
|
||||
onclose(callback: (error?: Error) => void): void;
|
||||
@@ -93,6 +94,30 @@ export class SignalRConnection {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a hub method after the lazy connection has started.
|
||||
* Returns false when the hub cannot be reached, so callers can degrade
|
||||
* quietly while keeping already-fetched data visible.
|
||||
*/
|
||||
async invoke(methodName: string, ...args: unknown[]): Promise<boolean> {
|
||||
if (!this.buildPromise) {
|
||||
this.buildPromise = this.buildAndStart();
|
||||
}
|
||||
await this.buildPromise;
|
||||
|
||||
if (this._status !== "live" || !this.connection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.connection.invoke(methodName, ...args);
|
||||
return true;
|
||||
} catch {
|
||||
this.setStatus("offline");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Listen for connection status changes. Returns an unsubscribe function. */
|
||||
onStatusChange(handler: StatusHandler): () => void {
|
||||
this.statusHandlers.add(handler);
|
||||
|
||||
Reference in New Issue
Block a user