[feat](client): address one instance origin instead of connection modes
Apps reach every service through Maestro on one port; direct service addressing is no longer a supported deployment, and Maestro has no per-service hostname or host-port route. Replace the maestro/direct modes and the serviceRef lookup with one configured origin and the fixed paths /api/alarm/{action,auth,ws}, defaulting dev builds to 127.0.0.1:8080 through adb reverse. The old mode-specific body wrapping and the direct-mode socket branch go with it.
This commit is contained in:
+44
-14
@@ -7,7 +7,7 @@ import {
|
||||
type SocketState,
|
||||
} from '../services/alarmSocket';
|
||||
import { alarmSocketUrl, type ResolvedConnection } from '../services/connection';
|
||||
import { alarmStatus as fetchAlarmStatus, type AlarmStatus } from '../services/alarmApi';
|
||||
import { alarmStatus as fetchAlarmStatus, isAlertGone, type AlarmStatus } from '../services/alarmApi';
|
||||
|
||||
export type LiveDelivery = 'socket' | 'polling' | 'unavailable';
|
||||
|
||||
@@ -22,6 +22,10 @@ export interface UseAlarmSocketOptions {
|
||||
onTrigger: (frame: AlarmFrame) => void;
|
||||
onDisarm: (frame: AlarmFrame) => void;
|
||||
enabled?: boolean;
|
||||
/** This device's ledger id (the service hashes it to address the debt). */
|
||||
deviceId?: string | null;
|
||||
/** The service answered 404: the alert is destroyed, there is nothing to ring. */
|
||||
onGone?: () => void;
|
||||
}
|
||||
|
||||
export interface UseAlarmSocketResult {
|
||||
@@ -36,9 +40,11 @@ export interface UseAlarmSocketResult {
|
||||
/**
|
||||
* Owns the live-alarm transport for one subscribed alert.
|
||||
*
|
||||
* Direct mode gets the WebSocket (the only mode Maestro can pass through);
|
||||
* maestro mode is polling-only and reports `delivery: 'unavailable'` so the
|
||||
* Settings/health screen can say so out loud instead of pretending.
|
||||
* The WebSocket is the delivery path: triggers arrive only over
|
||||
* `ws(s)://{instance}/api/alarm/ws/alarm`. `delivery: 'unavailable'` means no
|
||||
* instance is configured, and the UI must say this device is not listening
|
||||
* rather than implying coverage. The status poll below is a display
|
||||
* reconciliation only — it never becomes a delivery claim.
|
||||
*/
|
||||
export function useAlarmSocket({
|
||||
resolved,
|
||||
@@ -49,6 +55,8 @@ export function useAlarmSocket({
|
||||
onTrigger,
|
||||
onDisarm,
|
||||
enabled = true,
|
||||
deviceId,
|
||||
onGone,
|
||||
}: UseAlarmSocketOptions): UseAlarmSocketResult {
|
||||
const [socketState, setSocketState] = useState<SocketState>('idle');
|
||||
const [attempts, setAttempts] = useState(0);
|
||||
@@ -66,17 +74,36 @@ export function useAlarmSocket({
|
||||
[resolved, alertId, subscribeToken],
|
||||
);
|
||||
|
||||
/** Capability, not liveness: the URL exists. Liveness is `socketState`. */
|
||||
const delivery: LiveDelivery = url ? 'socket' : 'unavailable';
|
||||
|
||||
/**
|
||||
* A check-in can carry the durable "fired while you were offline" record
|
||||
* (409 `ERR_ALARM_TRIGGERED_OFFLINE`, mapped to a success-shaped status by
|
||||
* `alarmStatus`). Ack it over the socket when one is open, then let the
|
||||
* caller's status effect raise the takeover; a closed socket is fine because
|
||||
* the service clears the pending entry on the check-in response itself.
|
||||
*/
|
||||
const onGoneRef = useRef<UseAlarmSocketOptions['onGone']>(undefined);
|
||||
onGoneRef.current = onGone;
|
||||
|
||||
const applyStatus = useCallback((status: AlarmStatus) => {
|
||||
setLastStatus(status);
|
||||
if (status.offlineTrigger && status.eventId) {
|
||||
socketRef.current?.ack(status.eventId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!alertId) return;
|
||||
try {
|
||||
setLastStatus(await fetchAlarmStatus(alertId));
|
||||
} catch {
|
||||
applyStatus(await fetchAlarmStatus(alertId, deviceId ?? undefined));
|
||||
} catch (e) {
|
||||
if (isAlertGone(e)) onGoneRef.current?.();
|
||||
// A failed status read is not fatal — the socket is the primary path and
|
||||
// the next poll retries.
|
||||
}
|
||||
}, [alertId]);
|
||||
}, [alertId, applyStatus, deviceId]);
|
||||
|
||||
// AppState awareness: while backgrounded the JS socket is unreliable, so the
|
||||
// poll cadence relaxes to the background value and the socket is left to the
|
||||
@@ -112,8 +139,10 @@ export function useAlarmSocket({
|
||||
};
|
||||
}, [enabled, url, alertId, subscribeToken]);
|
||||
|
||||
// Polling is always on while armed: it is the fallback when the socket is not
|
||||
// available (maestro mode) and the safety net when it is momentarily down.
|
||||
// Status reconciliation. It never *delivers* a live trigger (the socket is the
|
||||
// alarm path), but it is also the check-in that carries the durable record of
|
||||
// an alarm fired while this device was offline — so a missed trigger comes
|
||||
// back as a late takeover instead of disappearing.
|
||||
useEffect(() => {
|
||||
if (!enabled || !alertId) return;
|
||||
const interval = appState === 'active' ? foregroundPollMs : backgroundPollMs;
|
||||
@@ -121,10 +150,11 @@ export function useAlarmSocket({
|
||||
const tick = async () => {
|
||||
if (cancelled) return;
|
||||
try {
|
||||
const status = await fetchAlarmStatus(alertId);
|
||||
if (!cancelled) setLastStatus(status);
|
||||
} catch {
|
||||
// ignore — polling must never surface a spinner
|
||||
const status = await fetchAlarmStatus(alertId, deviceId ?? undefined);
|
||||
if (!cancelled) applyStatus(status);
|
||||
} catch (e) {
|
||||
if (isAlertGone(e)) onGoneRef.current?.();
|
||||
// ignore otherwise — polling must never surface a spinner
|
||||
}
|
||||
};
|
||||
tick();
|
||||
@@ -133,7 +163,7 @@ export function useAlarmSocket({
|
||||
cancelled = true;
|
||||
clearInterval(handle);
|
||||
};
|
||||
}, [enabled, alertId, appState, foregroundPollMs, backgroundPollMs]);
|
||||
}, [enabled, alertId, appState, foregroundPollMs, backgroundPollMs, applyStatus, deviceId]);
|
||||
|
||||
return { socketState, delivery, attempts, lastStatus, refresh };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user