fix(ws): stabilization try 6 + typo

This commit is contained in:
devthejo 2026-01-18 16:22:42 +01:00
parent ef643f77cb
commit 5dfb064c2c
No known key found for this signature in database
GPG key ID: 00CCA7A92B1D5351
5 changed files with 59 additions and 4 deletions

View file

@ -3,6 +3,7 @@ import useStreamQueryWithSubscription from "~/hooks/useStreamQueryWithSubscripti
import { aggregatedMessagesActions } from "~/stores"; import { aggregatedMessagesActions } from "~/stores";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { FEATURE_SCOPES, NETWORK_SCOPES } from "~/lib/logger/scopes"; import { FEATURE_SCOPES, NETWORK_SCOPES } from "~/lib/logger/scopes";
import { AppState } from "react-native";
import { import {
AGGREGATED_MESSAGES_QUERY, AGGREGATED_MESSAGES_QUERY,
@ -17,12 +18,14 @@ const messagesLogger = createLogger({
const AggregatedMessagesSubscription = () => { const AggregatedMessagesSubscription = () => {
// Ref to track if we've already run the cleanup // Ref to track if we've already run the cleanup
const initRunRef = useRef(false); const initRunRef = useRef(false);
const lastForegroundCatchupAtRef = useRef(0);
// Aggregated messages subscription // Aggregated messages subscription
const { const {
data: messagesData, data: messagesData,
error: messagesError, error: messagesError,
loading, loading,
refetch,
} = useStreamQueryWithSubscription( } = useStreamQueryWithSubscription(
AGGREGATED_MESSAGES_QUERY, AGGREGATED_MESSAGES_QUERY,
AGGREGATED_MESSAGES_SUBSCRIPTION, AGGREGATED_MESSAGES_SUBSCRIPTION,
@ -47,6 +50,37 @@ const AggregatedMessagesSubscription = () => {
}, },
); );
// Foreground catch-up: on mobile, WS can take time to resume after background.
// Do a lightweight refresh shortly after the app becomes active.
useEffect(() => {
const sub = AppState.addEventListener("change", (next) => {
if (next !== "active") return;
const now = Date.now();
// Avoid spamming refetches if the app toggles state quickly.
if (now - lastForegroundCatchupAtRef.current < 15_000) return;
lastForegroundCatchupAtRef.current = now;
if (!refetch) return;
try {
messagesLogger.info(
"Foreground catch-up: refetching aggregated messages",
);
Promise.resolve()
.then(() => refetch())
.catch((e) => {
messagesLogger.warn("Foreground catch-up refetch failed", {
error: e?.message,
});
});
} catch (_e) {
// ignore
}
});
return () => sub.remove();
}, [refetch]);
// Update loading state // Update loading state
useEffect(() => { useEffect(() => {
aggregatedMessagesActions.setLoading(loading); aggregatedMessagesActions.setLoading(loading);

View file

@ -140,7 +140,12 @@ export default function useLatestWithSubscription(
`[${subscriptionKey}] WS reconnect detected, refetching base query to prevent gaps`, `[${subscriptionKey}] WS reconnect detected, refetching base query to prevent gaps`,
{ wsClosedDate }, { wsClosedDate },
); );
await refetch(); // Don't block re-subscription forever if refetch is slow/stuck.
const maxWaitMs = 8000;
await Promise.race([
Promise.resolve().then(() => refetch()),
new Promise((resolve) => setTimeout(resolve, maxWaitMs)),
]);
} catch (e) { } catch (e) {
console.warn( console.warn(
`[${subscriptionKey}] Refetch-on-reconnect failed (continuing with resubscribe)`, `[${subscriptionKey}] Refetch-on-reconnect failed (continuing with resubscribe)`,

View file

@ -143,7 +143,12 @@ export default function useStreamQueryWithSubscription(
`[${subscriptionKey}] WS reconnect detected, refetching base query to prevent gaps`, `[${subscriptionKey}] WS reconnect detected, refetching base query to prevent gaps`,
{ wsClosedDate }, { wsClosedDate },
); );
await refetch(); // Don't block re-subscription forever if refetch is slow/stuck.
const maxWaitMs = 8000;
await Promise.race([
Promise.resolve().then(() => refetch()),
new Promise((resolve) => setTimeout(resolve, maxWaitMs)),
]);
} catch (e) { } catch (e) {
console.warn( console.warn(
`[${subscriptionKey}] Refetch-on-reconnect failed (continuing with resubscribe)`, `[${subscriptionKey}] Refetch-on-reconnect failed (continuing with resubscribe)`,
@ -800,5 +805,6 @@ export default function useStreamQueryWithSubscription(
data: queryData, data: queryData,
loading, loading,
error, error,
refetch,
}; };
} }

View file

@ -3,6 +3,7 @@ import { print } from "graphql";
// import { createClient } from "graphql-ws"; // import { createClient } from "graphql-ws";
import { createRestartableClient } from "./graphqlWs"; import { createRestartableClient } from "./graphqlWs";
import network from "~/network"; import network from "~/network";
import { networkActions } from "~/stores";
export default class WebSocketLink extends ApolloLink { export default class WebSocketLink extends ApolloLink {
constructor(options) { constructor(options) {
@ -18,7 +19,16 @@ export default class WebSocketLink extends ApolloLink {
return this.client.subscribe( return this.client.subscribe(
{ ...operation, query: print(operation.query) }, { ...operation, query: print(operation.query) },
{ {
next: sink.next.bind(sink), next: (value) => {
// Any subscription payload means the transport is alive.
// Touch WS heartbeat so watchdog/liveness logic doesn't falsely conclude staleness.
try {
networkActions.WSTouch();
} catch (_e) {
// ignore
}
sink.next(value);
},
complete: sink.complete.bind(sink), complete: sink.complete.bind(sink),
error: (err) => { error: (err) => {
// Don't propagate client restart events as errors // Don't propagate client restart events as errors

View file

@ -15,7 +15,7 @@ export default createAtom(({ merge, getActions }) => {
const networkActions = getActions("network"); const networkActions = getActions("network");
const alertActions = getActions("alert"); const alertActions = getActions("alert");
const navActions = getActions("alert"); const navActions = getActions("nav");
const fcmActions = getActions("fcm"); const fcmActions = getActions("fcm");
const paramsActions = getActions("params"); const paramsActions = getActions("params");
const notificationsActions = getActions("notifications"); const notificationsActions = getActions("notifications");