Compare commits
2 commits
8ba858f876
...
0cf1139f9b
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cf1139f9b | |||
| 39d2ede295 |
7 changed files with 400 additions and 141 deletions
|
|
@ -2,8 +2,15 @@ import { useRef, useEffect, useMemo, useState } from "react";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import * as Sentry from "@sentry/react-native";
|
import * as Sentry from "@sentry/react-native";
|
||||||
import { useNetworkState } from "~/stores";
|
import { useNetworkState } from "~/stores";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { UI_SCOPES } from "~/lib/logger/scopes";
|
||||||
import useShallowMemo from "./useShallowMemo";
|
import useShallowMemo from "./useShallowMemo";
|
||||||
|
|
||||||
|
const hookLogger = createLogger({
|
||||||
|
module: UI_SCOPES.HOOKS,
|
||||||
|
feature: "useLatestWithSubscription",
|
||||||
|
});
|
||||||
|
|
||||||
// Constants for retry configuration
|
// Constants for retry configuration
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
const INITIAL_BACKOFF_MS = 1000; // 1 second
|
const INITIAL_BACKOFF_MS = 1000; // 1 second
|
||||||
|
|
@ -47,13 +54,15 @@ export default function useLatestWithSubscription(
|
||||||
const retryCountRef = useRef(0);
|
const retryCountRef = useRef(0);
|
||||||
const subscriptionErrorRef = useRef(null);
|
const subscriptionErrorRef = useRef(null);
|
||||||
const timeoutIdRef = useRef(null);
|
const timeoutIdRef = useRef(null);
|
||||||
|
const unsubscribeRef = useRef(null);
|
||||||
|
const lastWsClosedDateRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentVarsHash = JSON.stringify(variables);
|
const currentVarsHash = JSON.stringify(variables);
|
||||||
if (currentVarsHash !== variableHashRef.current) {
|
if (currentVarsHash !== variableHashRef.current) {
|
||||||
console.log(
|
hookLogger.debug("Variables changed; resetting subscription setup", {
|
||||||
`[${subscriptionKey}] Variables changed, resetting subscription setup`,
|
subscriptionKey,
|
||||||
);
|
});
|
||||||
highestIdRef.current = null;
|
highestIdRef.current = null;
|
||||||
variableHashRef.current = currentVarsHash;
|
variableHashRef.current = currentVarsHash;
|
||||||
initialSetupDoneRef.current = false;
|
initialSetupDoneRef.current = false;
|
||||||
|
|
@ -98,19 +107,19 @@ export default function useLatestWithSubscription(
|
||||||
(highestIdRef.current === null || highestId > highestIdRef.current)
|
(highestIdRef.current === null || highestId > highestIdRef.current)
|
||||||
) {
|
) {
|
||||||
highestIdRef.current = highestId;
|
highestIdRef.current = highestId;
|
||||||
console.log(
|
hookLogger.debug("Updated subscription cursor to highest ID", {
|
||||||
`[${subscriptionKey}] Updated subscription cursor to highest ID:`,
|
subscriptionKey,
|
||||||
highestId,
|
highestId,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle empty results case - initialize with 0 to allow subscription for first item
|
// Handle empty results case - initialize with 0 to allow subscription for first item
|
||||||
if (highestIdRef.current === null) {
|
if (highestIdRef.current === null) {
|
||||||
highestIdRef.current = 0;
|
highestIdRef.current = 0;
|
||||||
console.log(
|
hookLogger.debug("No initial items; setting subscription cursor", {
|
||||||
`[${subscriptionKey}] No initial items, setting subscription cursor to:`,
|
subscriptionKey,
|
||||||
0,
|
highestId: 0,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [queryData, cursorKey, subscriptionKey]);
|
}, [queryData, cursorKey, subscriptionKey]);
|
||||||
|
|
@ -134,12 +143,20 @@ export default function useLatestWithSubscription(
|
||||||
if (!subscribeToMore) return;
|
if (!subscribeToMore) return;
|
||||||
if (highestIdRef.current === null) return; // Wait until we have the highest ID
|
if (highestIdRef.current === null) return; // Wait until we have the highest ID
|
||||||
|
|
||||||
|
// Track WS close events so we only react when wsClosedDate actually changes
|
||||||
|
const wsClosedDateChanged =
|
||||||
|
!!wsClosedDate && wsClosedDate !== lastWsClosedDateRef.current;
|
||||||
|
if (wsClosedDateChanged) {
|
||||||
|
lastWsClosedDateRef.current = wsClosedDate;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if max retries reached and we have an error
|
// Check if max retries reached and we have an error
|
||||||
if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) {
|
if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) {
|
||||||
console.error(
|
hookLogger.error("Max retries reached; stopping subscription attempts", {
|
||||||
`[${subscriptionKey}] Max retries (${maxRetries}) reached. Stopping subscription attempts.`,
|
subscriptionKey,
|
||||||
subscriptionErrorRef.current,
|
maxRetries,
|
||||||
);
|
error: subscriptionErrorRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
// Report to Sentry when max retries are reached
|
// Report to Sentry when max retries are reached
|
||||||
try {
|
try {
|
||||||
|
|
@ -155,17 +172,24 @@ export default function useLatestWithSubscription(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (sentryError) {
|
} catch (sentryError) {
|
||||||
console.error("Failed to report to Sentry:", sentryError);
|
hookLogger.error("Failed to report max-retries to Sentry", {
|
||||||
|
subscriptionKey,
|
||||||
|
error: sentryError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for:
|
// Wait for:
|
||||||
// - either initial setup not done yet
|
// - initial setup not done yet
|
||||||
// - or a new wsClosedDate (WS reconnect)
|
// - OR a new wsClosedDate (WS reconnect)
|
||||||
// - or a retry trigger
|
// - OR a retry trigger
|
||||||
if (initialSetupDoneRef.current && !wsClosedDate && retryTrigger === 0) {
|
if (
|
||||||
|
initialSetupDoneRef.current &&
|
||||||
|
!wsClosedDateChanged &&
|
||||||
|
retryTrigger === 0
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,6 +202,16 @@ export default function useLatestWithSubscription(
|
||||||
timeoutIdRef.current = null;
|
timeoutIdRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always cleanup any existing subscription before creating a new one
|
||||||
|
if (unsubscribeRef.current) {
|
||||||
|
try {
|
||||||
|
unsubscribeRef.current();
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
unsubscribeRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate backoff delay if this is a retry
|
// Calculate backoff delay if this is a retry
|
||||||
const backoffDelay =
|
const backoffDelay =
|
||||||
retryCountRef.current > 0
|
retryCountRef.current > 0
|
||||||
|
|
@ -187,15 +221,13 @@ export default function useLatestWithSubscription(
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const retryMessage =
|
hookLogger.debug("Setting up subscription", {
|
||||||
retryCountRef.current > 0
|
subscriptionKey,
|
||||||
? ` Retry attempt ${retryCountRef.current}/${maxRetries} after ${backoffDelay}ms delay`
|
retryCount: retryCountRef.current,
|
||||||
: "";
|
maxRetries,
|
||||||
|
backoffDelay,
|
||||||
console.log(
|
highestId: highestIdRef.current,
|
||||||
`[${subscriptionKey}] Setting up subscription${retryMessage} with highestId:`,
|
});
|
||||||
highestIdRef.current,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use timeout for backoff
|
// Use timeout for backoff
|
||||||
timeoutIdRef.current = setTimeout(() => {
|
timeoutIdRef.current = setTimeout(() => {
|
||||||
|
|
@ -222,10 +254,12 @@ export default function useLatestWithSubscription(
|
||||||
maxRetries,
|
maxRetries,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.error(
|
hookLogger.warn("Subscription error", {
|
||||||
`[${subscriptionKey}] Subscription error (attempt ${retryCountRef.current}/${maxRetries}):`,
|
subscriptionKey,
|
||||||
|
attempt: retryCountRef.current,
|
||||||
|
maxRetries,
|
||||||
error,
|
error,
|
||||||
);
|
});
|
||||||
|
|
||||||
// If we haven't reached max retries, trigger a retry
|
// If we haven't reached max retries, trigger a retry
|
||||||
if (retryCountRef.current < maxRetries) {
|
if (retryCountRef.current < maxRetries) {
|
||||||
|
|
@ -270,10 +304,11 @@ export default function useLatestWithSubscription(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
hookLogger.debug("Received new items", {
|
||||||
`[${subscriptionKey}] Received ${filteredNewItems.length} new items, updated highestId:`,
|
subscriptionKey,
|
||||||
highestIdRef.current,
|
receivedCount: filteredNewItems.length,
|
||||||
);
|
highestId: highestIdRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
// For latest items pattern, we prepend new items (DESC order in UI)
|
// For latest items pattern, we prepend new items (DESC order in UI)
|
||||||
return {
|
return {
|
||||||
|
|
@ -283,30 +318,27 @@ export default function useLatestWithSubscription(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup on unmount or re-run
|
// Save unsubscribe for cleanup on reruns/unmount
|
||||||
return () => {
|
unsubscribeRef.current = unsubscribe;
|
||||||
console.log(`[${subscriptionKey}] Cleaning up subscription`);
|
|
||||||
if (timeoutIdRef.current) {
|
// Note: cleanup is handled by the effect cleanup below.
|
||||||
clearTimeout(timeoutIdRef.current);
|
|
||||||
timeoutIdRef.current = null;
|
|
||||||
}
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle setup errors (like malformed queries)
|
// Handle setup errors (like malformed queries)
|
||||||
console.error(
|
hookLogger.error("Error setting up subscription", {
|
||||||
`[${subscriptionKey}] Error setting up subscription:`,
|
subscriptionKey,
|
||||||
error,
|
error,
|
||||||
);
|
});
|
||||||
subscriptionErrorRef.current = error;
|
subscriptionErrorRef.current = error;
|
||||||
|
|
||||||
// Increment retry counter but don't exceed maxRetries
|
// Increment retry counter but don't exceed maxRetries
|
||||||
retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries);
|
retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries);
|
||||||
|
|
||||||
console.error(
|
hookLogger.warn("Subscription setup error", {
|
||||||
`[${subscriptionKey}] Subscription setup error (attempt ${retryCountRef.current}/${maxRetries}):`,
|
subscriptionKey,
|
||||||
|
attempt: retryCountRef.current,
|
||||||
|
maxRetries,
|
||||||
error,
|
error,
|
||||||
);
|
});
|
||||||
|
|
||||||
// If we haven't reached max retries, trigger a retry
|
// If we haven't reached max retries, trigger a retry
|
||||||
if (retryCountRef.current < maxRetries) {
|
if (retryCountRef.current < maxRetries) {
|
||||||
|
|
@ -328,16 +360,14 @@ export default function useLatestWithSubscription(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (sentryError) {
|
} catch (sentryError) {
|
||||||
console.error("Failed to report to Sentry:", sentryError);
|
hookLogger.error("Failed to report setup error to Sentry", {
|
||||||
|
subscriptionKey,
|
||||||
|
error: sentryError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
// Cleanup is handled by the effect cleanup below.
|
||||||
if (timeoutIdRef.current) {
|
|
||||||
clearTimeout(timeoutIdRef.current);
|
|
||||||
timeoutIdRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, backoffDelay);
|
}, backoffDelay);
|
||||||
|
|
||||||
|
|
@ -347,6 +377,16 @@ export default function useLatestWithSubscription(
|
||||||
clearTimeout(timeoutIdRef.current);
|
clearTimeout(timeoutIdRef.current);
|
||||||
timeoutIdRef.current = null;
|
timeoutIdRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (unsubscribeRef.current) {
|
||||||
|
try {
|
||||||
|
hookLogger.debug("Cleaning up subscription", { subscriptionKey });
|
||||||
|
unsubscribeRef.current();
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
unsubscribeRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
skip,
|
skip,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,15 @@ import { useRef, useEffect, useMemo, useState } from "react";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import * as Sentry from "@sentry/react-native";
|
import * as Sentry from "@sentry/react-native";
|
||||||
import { useNetworkState } from "~/stores";
|
import { useNetworkState } from "~/stores";
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { UI_SCOPES } from "~/lib/logger/scopes";
|
||||||
import useShallowMemo from "./useShallowMemo";
|
import useShallowMemo from "./useShallowMemo";
|
||||||
|
|
||||||
|
const hookLogger = createLogger({
|
||||||
|
module: UI_SCOPES.HOOKS,
|
||||||
|
feature: "useStreamQueryWithSubscription",
|
||||||
|
});
|
||||||
|
|
||||||
// Constants for retry configuration
|
// Constants for retry configuration
|
||||||
const MAX_RETRIES = 5;
|
const MAX_RETRIES = 5;
|
||||||
const INITIAL_BACKOFF_MS = 1000; // 1 second
|
const INITIAL_BACKOFF_MS = 1000; // 1 second
|
||||||
|
|
@ -40,14 +47,16 @@ export default function useStreamQueryWithSubscription(
|
||||||
const retryCountRef = useRef(0);
|
const retryCountRef = useRef(0);
|
||||||
const subscriptionErrorRef = useRef(null);
|
const subscriptionErrorRef = useRef(null);
|
||||||
const timeoutIdRef = useRef(null);
|
const timeoutIdRef = useRef(null);
|
||||||
|
const unsubscribeRef = useRef(null);
|
||||||
|
const lastWsClosedDateRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentVarsHash = JSON.stringify(variables);
|
const currentVarsHash = JSON.stringify(variables);
|
||||||
if (currentVarsHash !== variableHashRef.current) {
|
if (currentVarsHash !== variableHashRef.current) {
|
||||||
console.log(
|
hookLogger.debug("Variables changed; resetting cursor", {
|
||||||
`[${subscriptionKey}] Variables changed, resetting cursor to initial value:`,
|
subscriptionKey,
|
||||||
initialCursor,
|
initialCursor,
|
||||||
);
|
});
|
||||||
lastCursorRef.current = initialCursor;
|
lastCursorRef.current = initialCursor;
|
||||||
variableHashRef.current = currentVarsHash;
|
variableHashRef.current = currentVarsHash;
|
||||||
initialSetupDoneRef.current = false;
|
initialSetupDoneRef.current = false;
|
||||||
|
|
@ -99,10 +108,10 @@ export default function useStreamQueryWithSubscription(
|
||||||
const newCursor = lastItem[cursorKey];
|
const newCursor = lastItem[cursorKey];
|
||||||
|
|
||||||
lastCursorRef.current = newCursor;
|
lastCursorRef.current = newCursor;
|
||||||
console.log(
|
hookLogger.debug("Updated subscription cursor", {
|
||||||
`[${subscriptionKey}] Updated subscription cursor:`,
|
subscriptionKey,
|
||||||
newCursor,
|
cursor: newCursor,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}, [queryData, cursorKey, subscriptionKey]);
|
}, [queryData, cursorKey, subscriptionKey]);
|
||||||
|
|
||||||
|
|
@ -124,12 +133,20 @@ export default function useStreamQueryWithSubscription(
|
||||||
if (skip) return; // If skipping, do nothing
|
if (skip) return; // If skipping, do nothing
|
||||||
if (!subscribeToMore) return;
|
if (!subscribeToMore) return;
|
||||||
|
|
||||||
|
// Track WS close events so we only react when wsClosedDate actually changes
|
||||||
|
const wsClosedDateChanged =
|
||||||
|
!!wsClosedDate && wsClosedDate !== lastWsClosedDateRef.current;
|
||||||
|
if (wsClosedDateChanged) {
|
||||||
|
lastWsClosedDateRef.current = wsClosedDate;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if max retries reached and we have an error - this check must be done regardless of other conditions
|
// Check if max retries reached and we have an error - this check must be done regardless of other conditions
|
||||||
if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) {
|
if (retryCountRef.current >= maxRetries && subscriptionErrorRef.current) {
|
||||||
console.error(
|
hookLogger.error("Max retries reached; stopping subscription attempts", {
|
||||||
`[${subscriptionKey}] Max retries (${maxRetries}) reached. Stopping subscription attempts.`,
|
subscriptionKey,
|
||||||
subscriptionErrorRef.current,
|
maxRetries,
|
||||||
);
|
error: subscriptionErrorRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
// Report to Sentry when max retries are reached
|
// Report to Sentry when max retries are reached
|
||||||
try {
|
try {
|
||||||
|
|
@ -145,17 +162,24 @@ export default function useStreamQueryWithSubscription(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (sentryError) {
|
} catch (sentryError) {
|
||||||
console.error("Failed to report to Sentry:", sentryError);
|
hookLogger.error("Failed to report max-retries to Sentry", {
|
||||||
|
subscriptionKey,
|
||||||
|
error: sentryError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for:
|
// Wait for:
|
||||||
// - either initial setup not done yet
|
// - initial setup not done yet
|
||||||
// - or a new wsClosedDate (WS reconnect)
|
// - OR a new wsClosedDate (WS reconnect)
|
||||||
// - or a retry trigger
|
// - OR a retry trigger
|
||||||
if (initialSetupDoneRef.current && !wsClosedDate && retryTrigger === 0) {
|
if (
|
||||||
|
initialSetupDoneRef.current &&
|
||||||
|
!wsClosedDateChanged &&
|
||||||
|
retryTrigger === 0
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +192,16 @@ export default function useStreamQueryWithSubscription(
|
||||||
timeoutIdRef.current = null;
|
timeoutIdRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always cleanup any existing subscription before creating a new one
|
||||||
|
if (unsubscribeRef.current) {
|
||||||
|
try {
|
||||||
|
unsubscribeRef.current();
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
unsubscribeRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate backoff delay if this is a retry
|
// Calculate backoff delay if this is a retry
|
||||||
const backoffDelay =
|
const backoffDelay =
|
||||||
retryCountRef.current > 0
|
retryCountRef.current > 0
|
||||||
|
|
@ -177,15 +211,13 @@ export default function useStreamQueryWithSubscription(
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const retryMessage =
|
hookLogger.debug("Setting up subscription", {
|
||||||
retryCountRef.current > 0
|
subscriptionKey,
|
||||||
? ` Retry attempt ${retryCountRef.current}/${maxRetries} after ${backoffDelay}ms delay`
|
retryCount: retryCountRef.current,
|
||||||
: "";
|
maxRetries,
|
||||||
|
backoffDelay,
|
||||||
console.log(
|
cursor: lastCursorRef.current,
|
||||||
`[${subscriptionKey}] Setting up subscription${retryMessage} with cursor:`,
|
});
|
||||||
lastCursorRef.current,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use timeout for backoff
|
// Use timeout for backoff
|
||||||
timeoutIdRef.current = setTimeout(() => {
|
timeoutIdRef.current = setTimeout(() => {
|
||||||
|
|
@ -212,10 +244,12 @@ export default function useStreamQueryWithSubscription(
|
||||||
maxRetries,
|
maxRetries,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.error(
|
hookLogger.warn("Subscription error", {
|
||||||
`[${subscriptionKey}] Subscription error (attempt ${retryCountRef.current}/${maxRetries}):`,
|
subscriptionKey,
|
||||||
|
attempt: retryCountRef.current,
|
||||||
|
maxRetries,
|
||||||
error,
|
error,
|
||||||
);
|
});
|
||||||
|
|
||||||
// If we haven't reached max retries, trigger a retry
|
// If we haven't reached max retries, trigger a retry
|
||||||
if (retryCountRef.current < maxRetries) {
|
if (retryCountRef.current < maxRetries) {
|
||||||
|
|
@ -263,10 +297,10 @@ export default function useStreamQueryWithSubscription(
|
||||||
newItemCursor > lastCursorRef.current
|
newItemCursor > lastCursorRef.current
|
||||||
) {
|
) {
|
||||||
lastCursorRef.current = newItemCursor;
|
lastCursorRef.current = newItemCursor;
|
||||||
console.log(
|
hookLogger.debug("Received item; cursor advanced", {
|
||||||
`[${subscriptionKey}] New message received with cursor:`,
|
subscriptionKey,
|
||||||
lastCursorRef.current,
|
cursor: lastCursorRef.current,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = itemMap.get(item[uniqKey]);
|
const existing = itemMap.get(item[uniqKey]);
|
||||||
|
|
@ -289,30 +323,27 @@ export default function useStreamQueryWithSubscription(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup on unmount or re-run
|
// Save unsubscribe for cleanup on reruns/unmount
|
||||||
return () => {
|
unsubscribeRef.current = unsubscribe;
|
||||||
console.log(`[${subscriptionKey}] Cleaning up subscription`);
|
|
||||||
if (timeoutIdRef.current) {
|
// Note: cleanup is handled by the effect cleanup below.
|
||||||
clearTimeout(timeoutIdRef.current);
|
|
||||||
timeoutIdRef.current = null;
|
|
||||||
}
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle setup errors (like malformed queries)
|
// Handle setup errors (like malformed queries)
|
||||||
console.error(
|
hookLogger.error("Error setting up subscription", {
|
||||||
`[${subscriptionKey}] Error setting up subscription:`,
|
subscriptionKey,
|
||||||
error,
|
error,
|
||||||
);
|
});
|
||||||
subscriptionErrorRef.current = error;
|
subscriptionErrorRef.current = error;
|
||||||
|
|
||||||
// Increment retry counter but don't exceed maxRetries
|
// Increment retry counter but don't exceed maxRetries
|
||||||
retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries);
|
retryCountRef.current = Math.min(retryCountRef.current + 1, maxRetries);
|
||||||
|
|
||||||
console.error(
|
hookLogger.warn("Subscription setup error", {
|
||||||
`[${subscriptionKey}] Subscription setup error (attempt ${retryCountRef.current}/${maxRetries}):`,
|
subscriptionKey,
|
||||||
|
attempt: retryCountRef.current,
|
||||||
|
maxRetries,
|
||||||
error,
|
error,
|
||||||
);
|
});
|
||||||
|
|
||||||
// If we haven't reached max retries, trigger a retry
|
// If we haven't reached max retries, trigger a retry
|
||||||
if (retryCountRef.current < maxRetries) {
|
if (retryCountRef.current < maxRetries) {
|
||||||
|
|
@ -334,16 +365,14 @@ export default function useStreamQueryWithSubscription(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (sentryError) {
|
} catch (sentryError) {
|
||||||
console.error("Failed to report to Sentry:", sentryError);
|
hookLogger.error("Failed to report setup error to Sentry", {
|
||||||
|
subscriptionKey,
|
||||||
|
error: sentryError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
// Cleanup is handled by the effect cleanup below.
|
||||||
if (timeoutIdRef.current) {
|
|
||||||
clearTimeout(timeoutIdRef.current);
|
|
||||||
timeoutIdRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, backoffDelay);
|
}, backoffDelay);
|
||||||
|
|
||||||
|
|
@ -353,6 +382,16 @@ export default function useStreamQueryWithSubscription(
|
||||||
clearTimeout(timeoutIdRef.current);
|
clearTimeout(timeoutIdRef.current);
|
||||||
timeoutIdRef.current = null;
|
timeoutIdRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (unsubscribeRef.current) {
|
||||||
|
try {
|
||||||
|
hookLogger.debug("Cleaning up subscription", { subscriptionKey });
|
||||||
|
unsubscribeRef.current();
|
||||||
|
} catch (_error) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
unsubscribeRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
skip,
|
skip,
|
||||||
|
|
|
||||||
|
|
@ -5,27 +5,40 @@ import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
|
||||||
import jwtDecode from "jwt-decode";
|
import jwtDecode from "jwt-decode";
|
||||||
import { initEmulatorMode } from "./emulatorService";
|
import { initEmulatorMode } from "./emulatorService";
|
||||||
|
|
||||||
import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
|
import {
|
||||||
|
getAlertState,
|
||||||
|
getAuthState,
|
||||||
|
getSessionState,
|
||||||
|
subscribeAlertState,
|
||||||
|
subscribeAuthState,
|
||||||
|
subscribeSessionState,
|
||||||
|
permissionsActions,
|
||||||
|
} from "~/stores";
|
||||||
|
|
||||||
import setLocationState from "~/location/setLocationState";
|
import setLocationState from "~/location/setLocationState";
|
||||||
import { storeLocation } from "~/location/storage";
|
import { storeLocation } from "~/location/storage";
|
||||||
|
|
||||||
import env from "~/env";
|
import env from "~/env";
|
||||||
|
|
||||||
const config = {
|
// Common config: keep always-on tracking enabled, but default to an IDLE low-power profile.
|
||||||
|
// High-accuracy and "moving" mode are only enabled when an active alert is open.
|
||||||
|
const baseConfig = {
|
||||||
// https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
|
// https://github.com/transistorsoft/react-native-background-geolocation/wiki/Android-Headless-Mode
|
||||||
enableHeadless: true,
|
enableHeadless: true,
|
||||||
disableProviderChangeRecord: true,
|
disableProviderChangeRecord: true,
|
||||||
// disableMotionActivityUpdates: true,
|
// disableMotionActivityUpdates: true,
|
||||||
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
|
// Default to low-power (idle) profile; will be overridden when needed.
|
||||||
distanceFilter: TRACK_MOVE,
|
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
|
||||||
|
// Larger distance filter in idle mode to prevent frequent GPS wakes.
|
||||||
|
distanceFilter: 200,
|
||||||
// debug: true, // Enable debug mode for more detailed logs
|
// debug: true, // Enable debug mode for more detailed logs
|
||||||
logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,
|
logLevel: BackgroundGeolocation.LOG_LEVEL_VERBOSE,
|
||||||
// Disable automatic permission requests
|
// Disable automatic permission requests
|
||||||
locationAuthorizationRequest: "Always",
|
locationAuthorizationRequest: "Always",
|
||||||
stopOnTerminate: false,
|
stopOnTerminate: false,
|
||||||
startOnBoot: true,
|
startOnBoot: true,
|
||||||
heartbeatInterval: 900,
|
// Keep heartbeat very infrequent in idle mode.
|
||||||
|
heartbeatInterval: 3600,
|
||||||
// Force the plugin to start aggressively
|
// Force the plugin to start aggressively
|
||||||
foregroundService: true,
|
foregroundService: true,
|
||||||
notification: {
|
notification: {
|
||||||
|
|
@ -53,7 +66,19 @@ const config = {
|
||||||
autoSync: true,
|
autoSync: true,
|
||||||
reset: true,
|
reset: true,
|
||||||
};
|
};
|
||||||
const defaultConfig = config;
|
|
||||||
|
const TRACKING_PROFILES = {
|
||||||
|
idle: {
|
||||||
|
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_LOW,
|
||||||
|
distanceFilter: 200,
|
||||||
|
heartbeatInterval: 3600,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
desiredAccuracy: BackgroundGeolocation.DESIRED_ACCURACY_HIGH,
|
||||||
|
distanceFilter: TRACK_MOVE,
|
||||||
|
heartbeatInterval: 900,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default async function trackLocation() {
|
export default async function trackLocation() {
|
||||||
const locationLogger = createLogger({
|
const locationLogger = createLogger({
|
||||||
|
|
@ -61,6 +86,66 @@ export default async function trackLocation() {
|
||||||
feature: "tracking",
|
feature: "tracking",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let currentProfile = null;
|
||||||
|
let authReady = false;
|
||||||
|
let stopAlertSubscription = null;
|
||||||
|
let stopSessionSubscription = null;
|
||||||
|
|
||||||
|
const computeHasOwnOpenAlert = () => {
|
||||||
|
try {
|
||||||
|
const { userId } = getSessionState();
|
||||||
|
const { alertingList } = getAlertState();
|
||||||
|
if (!userId || !Array.isArray(alertingList)) return false;
|
||||||
|
return alertingList.some(
|
||||||
|
({ oneAlert }) =>
|
||||||
|
oneAlert?.state === "open" && oneAlert?.userId === userId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
locationLogger.warn("Failed to compute active-alert state", {
|
||||||
|
error: e?.message,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyProfile = async (profileName) => {
|
||||||
|
if (!authReady) {
|
||||||
|
// We only apply profile once auth headers are configured.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentProfile === profileName) return;
|
||||||
|
|
||||||
|
const profile = TRACKING_PROFILES[profileName];
|
||||||
|
if (!profile) {
|
||||||
|
locationLogger.warn("Unknown tracking profile", { profileName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
locationLogger.info("Applying tracking profile", {
|
||||||
|
profileName,
|
||||||
|
desiredAccuracy: profile.desiredAccuracy,
|
||||||
|
distanceFilter: profile.distanceFilter,
|
||||||
|
heartbeatInterval: profile.heartbeatInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await BackgroundGeolocation.setConfig(profile);
|
||||||
|
|
||||||
|
// Key battery fix:
|
||||||
|
// - IDLE profile forces stationary mode
|
||||||
|
// - ACTIVE profile forces moving mode
|
||||||
|
await BackgroundGeolocation.changePace(profileName === "active");
|
||||||
|
|
||||||
|
currentProfile = profileName;
|
||||||
|
} catch (error) {
|
||||||
|
locationLogger.error("Failed to apply tracking profile", {
|
||||||
|
profileName,
|
||||||
|
error: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Log the geolocation sync URL for debugging
|
// Log the geolocation sync URL for debugging
|
||||||
locationLogger.info("Geolocation sync URL configuration", {
|
locationLogger.info("Geolocation sync URL configuration", {
|
||||||
url: env.GEOLOC_SYNC_URL,
|
url: env.GEOLOC_SYNC_URL,
|
||||||
|
|
@ -76,6 +161,18 @@ export default async function trackLocation() {
|
||||||
locationLogger.info("No auth token, stopping location tracking");
|
locationLogger.info("No auth token, stopping location tracking");
|
||||||
await BackgroundGeolocation.stop();
|
await BackgroundGeolocation.stop();
|
||||||
locationLogger.debug("Location tracking stopped");
|
locationLogger.debug("Location tracking stopped");
|
||||||
|
|
||||||
|
// Cleanup subscriptions when logged out.
|
||||||
|
try {
|
||||||
|
stopAlertSubscription && stopAlertSubscription();
|
||||||
|
stopSessionSubscription && stopSessionSubscription();
|
||||||
|
} finally {
|
||||||
|
stopAlertSubscription = null;
|
||||||
|
stopSessionSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
authReady = false;
|
||||||
|
currentProfile = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// unsub();
|
// unsub();
|
||||||
|
|
@ -87,6 +184,8 @@ export default async function trackLocation() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
authReady = true;
|
||||||
|
|
||||||
// Log the authorization header that was set
|
// Log the authorization header that was set
|
||||||
locationLogger.debug(
|
locationLogger.debug(
|
||||||
"Set Authorization header for background geolocation",
|
"Set Authorization header for background geolocation",
|
||||||
|
|
@ -106,19 +205,7 @@ export default async function trackLocation() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.enabled) {
|
if (!state.enabled) {
|
||||||
locationLogger.info("Syncing location data");
|
|
||||||
try {
|
|
||||||
await BackgroundGeolocation.changePace(true);
|
|
||||||
await BackgroundGeolocation.sync();
|
|
||||||
locationLogger.debug("Sync initiated successfully");
|
|
||||||
} catch (error) {
|
|
||||||
locationLogger.error("Failed to sync location data", {
|
|
||||||
error: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
locationLogger.info("Starting location tracking");
|
locationLogger.info("Starting location tracking");
|
||||||
try {
|
try {
|
||||||
await BackgroundGeolocation.start();
|
await BackgroundGeolocation.start();
|
||||||
|
|
@ -130,6 +217,31 @@ export default async function trackLocation() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure we are NOT forcing "moving" mode by default.
|
||||||
|
// Default profile is idle unless an active alert requires higher accuracy.
|
||||||
|
const shouldBeActive = computeHasOwnOpenAlert();
|
||||||
|
await applyProfile(shouldBeActive ? "active" : "idle");
|
||||||
|
|
||||||
|
// Subscribe to changes that may require switching profiles.
|
||||||
|
if (!stopSessionSubscription) {
|
||||||
|
stopSessionSubscription = subscribeSessionState(
|
||||||
|
(s) => s?.userId,
|
||||||
|
() => {
|
||||||
|
const active = computeHasOwnOpenAlert();
|
||||||
|
applyProfile(active ? "active" : "idle");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!stopAlertSubscription) {
|
||||||
|
stopAlertSubscription = subscribeAlertState(
|
||||||
|
(s) => s?.alertingList,
|
||||||
|
() => {
|
||||||
|
const active = computeHasOwnOpenAlert();
|
||||||
|
applyProfile(active ? "active" : "idle");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackgroundGeolocation.onLocation(async (location) => {
|
BackgroundGeolocation.onLocation(async (location) => {
|
||||||
|
|
@ -161,8 +273,8 @@ export default async function trackLocation() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
locationLogger.info("Initializing background geolocation");
|
locationLogger.info("Initializing background geolocation");
|
||||||
await BackgroundGeolocation.ready(defaultConfig);
|
await BackgroundGeolocation.ready(baseConfig);
|
||||||
await BackgroundGeolocation.setConfig(config);
|
await BackgroundGeolocation.setConfig(baseConfig);
|
||||||
|
|
||||||
// Only set the permission state if we already have the permission
|
// Only set the permission state if we already have the permission
|
||||||
const state = await BackgroundGeolocation.getState();
|
const state = await BackgroundGeolocation.getState();
|
||||||
|
|
@ -203,6 +315,9 @@ export default async function trackLocation() {
|
||||||
subscribeAuthState(({ userToken }) => userToken, handleAuth);
|
subscribeAuthState(({ userToken }) => userToken, handleAuth);
|
||||||
locationLogger.debug("Performing initial auth handling");
|
locationLogger.debug("Performing initial auth handling");
|
||||||
handleAuth(userToken);
|
handleAuth(userToken);
|
||||||
// Initialize emulator mode if previously enabled
|
|
||||||
|
// Initialize emulator mode only in dev/staging to avoid accidental production battery drain.
|
||||||
|
if (__DEV__ || env.IS_STAGING) {
|
||||||
initEmulatorMode();
|
initEmulatorMode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,16 @@ import * as store from "~/stores";
|
||||||
|
|
||||||
import getRetryMaxAttempts from "./getRetryMaxAttemps";
|
import getRetryMaxAttempts from "./getRetryMaxAttemps";
|
||||||
|
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { NETWORK_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
const { useNetworkState, networkActions } = store;
|
const { useNetworkState, networkActions } = store;
|
||||||
|
|
||||||
|
const networkProvidersLogger = createLogger({
|
||||||
|
module: NETWORK_SCOPES.APOLLO,
|
||||||
|
feature: "NetworkProviders",
|
||||||
|
});
|
||||||
|
|
||||||
const initializeNewApolloClient = (reload) => {
|
const initializeNewApolloClient = (reload) => {
|
||||||
if (reload) {
|
if (reload) {
|
||||||
const { apolloClient } = network;
|
const { apolloClient } = network;
|
||||||
|
|
@ -47,6 +55,10 @@ export default function NetworkProviders({ children }) {
|
||||||
const networkState = useNetworkState(["initialized", "triggerReload"]);
|
const networkState = useNetworkState(["initialized", "triggerReload"]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (networkState.triggerReload) {
|
if (networkState.triggerReload) {
|
||||||
|
networkProvidersLogger.debug("Network triggerReload received", {
|
||||||
|
reloadId: store.getAuthState()?.reloadId,
|
||||||
|
hasUserToken: !!store.getAuthState()?.userToken,
|
||||||
|
});
|
||||||
initializeNewApolloClient(true);
|
initializeNewApolloClient(true);
|
||||||
setKey((prevKey) => prevKey + 1);
|
setKey((prevKey) => prevKey + 1);
|
||||||
}
|
}
|
||||||
|
|
@ -54,6 +66,10 @@ export default function NetworkProviders({ children }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (key > 0) {
|
if (key > 0) {
|
||||||
|
networkProvidersLogger.debug("Network reloaded", {
|
||||||
|
reloadId: store.getAuthState()?.reloadId,
|
||||||
|
hasUserToken: !!store.getAuthState()?.userToken,
|
||||||
|
});
|
||||||
networkActions.onReload();
|
networkActions.onReload();
|
||||||
}
|
}
|
||||||
}, [key]);
|
}, [key]);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { NETWORK_SCOPES } from "~/lib/logger/scopes";
|
||||||
import getStatusCode from "./getStatusCode";
|
import getStatusCode from "./getStatusCode";
|
||||||
import isAbortError from "./isAbortError";
|
import isAbortError from "./isAbortError";
|
||||||
|
|
||||||
|
import { getSessionState } from "~/stores";
|
||||||
|
|
||||||
let pendingRequests = [];
|
let pendingRequests = [];
|
||||||
|
|
||||||
const resolvePendingRequests = () => {
|
const resolvePendingRequests = () => {
|
||||||
|
|
@ -127,10 +129,31 @@ export default function createErrorLink({ store }) {
|
||||||
|
|
||||||
// Capture all other errors in Sentry
|
// Capture all other errors in Sentry
|
||||||
const errorMessage = `apollo error: ${getErrorMessage(error)}`;
|
const errorMessage = `apollo error: ${getErrorMessage(error)}`;
|
||||||
Sentry.captureException(new Error(errorMessage), {
|
|
||||||
extra: {
|
const authState = getAuthState();
|
||||||
errorObject: error,
|
const sessionState = getSessionState() || {};
|
||||||
|
|
||||||
|
// Keep Sentry context useful but avoid high-volume/PII payloads.
|
||||||
|
// - Don't attach the raw Apollo error object (can contain request details)
|
||||||
|
// - Don't attach identifiers (userId/deviceId)
|
||||||
|
// - Keep role info since it's relevant to the incident class
|
||||||
|
const safeExtras = {
|
||||||
|
operationName: operation.operationName,
|
||||||
|
statusCode,
|
||||||
|
reloadId: authState?.reloadId,
|
||||||
|
hasUserToken: !!authState?.userToken,
|
||||||
|
authLoading: !!authState?.loading,
|
||||||
|
session: {
|
||||||
|
initialized: !!sessionState.initialized,
|
||||||
|
defaultRole: sessionState.defaultRole,
|
||||||
|
allowedRolesCount: Array.isArray(sessionState.allowedRoles)
|
||||||
|
? sessionState.allowedRoles.length
|
||||||
|
: 0,
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Sentry.captureException(new Error(errorMessage), {
|
||||||
|
extra: safeExtras,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
};
|
};
|
||||||
const confirmLoginRequest = async ({ authTokenJwt, isConnected }) => {
|
const confirmLoginRequest = async ({ authTokenJwt, isConnected }) => {
|
||||||
authLogger.info("Confirming login request", { isConnected });
|
authLogger.info("Confirming login request", { isConnected });
|
||||||
|
const reloadId = Date.now();
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
// backup anon tokens
|
// backup anon tokens
|
||||||
const [anonAuthToken, anonUserToken] = await Promise.all([
|
const [anonAuthToken, anonUserToken] = await Promise.all([
|
||||||
|
|
@ -213,7 +214,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
secureStore.setItemAsync(STORAGE_KEYS.ANON_USER_TOKEN, anonUserToken),
|
secureStore.setItemAsync(STORAGE_KEYS.ANON_USER_TOKEN, anonUserToken),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
merge({ onReloadAuthToken: authTokenJwt });
|
merge({ onReloadAuthToken: authTokenJwt, reloadId });
|
||||||
triggerReload();
|
triggerReload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -308,6 +309,7 @@ export default createAtom(({ get, merge, getActions }) => {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
onReload: false,
|
onReload: false,
|
||||||
onReloadAuthToken: null,
|
onReloadAuthToken: null,
|
||||||
|
reloadId: null,
|
||||||
userOffMode: false,
|
userOffMode: false,
|
||||||
isReloading: false,
|
isReloading: false,
|
||||||
lastReloadTime: 0,
|
lastReloadTime: 0,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import { createAtom } from "~/lib/atomic-zustand";
|
import { createAtom } from "~/lib/atomic-zustand";
|
||||||
|
|
||||||
|
import { createLogger } from "~/lib/logger";
|
||||||
|
import { SYSTEM_SCOPES } from "~/lib/logger/scopes";
|
||||||
|
|
||||||
|
const treeLogger = createLogger({
|
||||||
|
module: SYSTEM_SCOPES.APP,
|
||||||
|
feature: "tree-reload",
|
||||||
|
});
|
||||||
|
|
||||||
const reloadCallbacks = [];
|
const reloadCallbacks = [];
|
||||||
|
|
||||||
export default createAtom(({ merge, getActions }) => {
|
export default createAtom(({ merge, getActions }) => {
|
||||||
|
|
@ -24,12 +32,14 @@ export default createAtom(({ merge, getActions }) => {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
reloadCallbacks.push(callback);
|
reloadCallbacks.push(callback);
|
||||||
}
|
}
|
||||||
networkActions.triggerReload();
|
// Clear session/store state first to stop user-level queries/subscriptions
|
||||||
|
// while we swap identity tokens.
|
||||||
sessionActions.clear();
|
sessionActions.clear();
|
||||||
resetStores();
|
resetStores();
|
||||||
merge({
|
merge({
|
||||||
triggerReload: true,
|
triggerReload: true,
|
||||||
suspend: false,
|
// Keep the tree suspended until we've run reload callbacks.
|
||||||
|
suspend: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -37,12 +47,26 @@ export default createAtom(({ merge, getActions }) => {
|
||||||
merge({
|
merge({
|
||||||
triggerReload: false,
|
triggerReload: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Run all reload callbacks sequentially and await them.
|
||||||
|
// This ensures auth identity swap completes BEFORE the network layer is recreated.
|
||||||
while (reloadCallbacks.length > 0) {
|
while (reloadCallbacks.length > 0) {
|
||||||
let callback = reloadCallbacks.shift();
|
let callback = reloadCallbacks.shift();
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback();
|
try {
|
||||||
|
await Promise.resolve(callback());
|
||||||
|
} catch (error) {
|
||||||
|
treeLogger.error("Reload callback threw", {
|
||||||
|
error: error?.message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
networkActions.triggerReload();
|
||||||
|
|
||||||
|
// Allow tree to render again; NetworkProviders will show its loader until ready.
|
||||||
|
merge({ suspend: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const suspendTree = () => {
|
const suspendTree = () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue