fix(io): headless

This commit is contained in:
Jo 2025-07-27 23:15:28 +02:00
parent 8183c7e4af
commit 6ea01c0c6d
Signed by: devthejo
GPG key ID: 00CCA7A92B1D5351
8 changed files with 107 additions and 49 deletions

View file

@ -1,6 +1,6 @@
import * as Location from "expo-location"; import * as Location from "expo-location";
import { useState, useRef, useEffect, useCallback } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import { storeLocation, getStoredLocation } from "~/utils/location/storage"; import { storeLocation, getStoredLocation } from "~/location/storage";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES, UI_SCOPES } from "~/lib/logger/scopes"; import { BACKGROUND_SCOPES, UI_SCOPES } from "~/lib/logger/scopes";

View file

@ -1,11 +1,15 @@
import { Platform } from "react-native";
import BackgroundGeolocation from "react-native-background-geolocation"; import BackgroundGeolocation from "react-native-background-geolocation";
import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage"; import { memoryAsyncStorage } from "~/storage/memoryAsyncStorage";
import { STORAGE_KEYS } from "~/storage/storageKeys"; import { STORAGE_KEYS } from "~/storage/storageKeys";
import { createLogger } from "~/lib/logger"; import { createLogger } from "~/lib/logger";
import { getStoredLocation } from "./storage";
import { getAuthState } from "~/stores";
import env from "~/env";
// Constants for persistence // Constants for persistence
// const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000; const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000;
const FORCE_SYNC_INTERVAL = 1 * 60 * 1000; // DEBUGGING // const FORCE_SYNC_INTERVAL = 1 * 60 * 1000; // DEBUGGING
const geolocBgLogger = createLogger({ const geolocBgLogger = createLogger({
service: "background-task", service: "background-task",
@ -35,38 +39,110 @@ const setLastSyncTime = async (time) => {
} }
}; };
// Shared heartbeat logic - mutualized between Android and iOS const executeSyncAndroid = async () => {
const executeSync = async () => { await BackgroundGeolocation.changePace(true);
let syncPerformed = false; await BackgroundGeolocation.sync();
let syncSuccessful = false; };
const executeSyncIOS = async () => {
try { try {
syncPerformed = true; const locationData = await getStoredLocation();
try { if (!locationData) {
// Change pace to ensure location updates geolocBgLogger.debug("No stored location data found, skipping sync");
await BackgroundGeolocation.changePace(true); return;
// Perform sync
await BackgroundGeolocation.sync();
syncSuccessful = true;
} catch (syncError) {
syncSuccessful = false;
} }
// Return result information for BackgroundFetch const { timestamp, coords } = locationData;
return {
syncPerformed, // Check if timestamp is too old (> 2 weeks)
syncSuccessful, const now = new Date();
const locationTime = new Date(timestamp);
const twoWeeksInMs = 14 * 24 * 60 * 60 * 1000; // 2 weeks in milliseconds
if (now - locationTime > twoWeeksInMs) {
geolocBgLogger.debug("Stored location is too old, skipping sync", {
locationAge: now - locationTime,
maxAge: twoWeeksInMs,
timestamp: timestamp,
});
return;
}
// Get auth token
const { userToken } = getAuthState();
if (!userToken) {
geolocBgLogger.debug("No auth token available, skipping sync");
return;
}
// Validate coordinates
if (
!coords ||
typeof coords.latitude !== "number" ||
typeof coords.longitude !== "number"
) {
geolocBgLogger.error("Invalid coordinates in stored location", {
coords,
});
return;
}
// Prepare payload according to API spec
const payload = {
location: {
event: "heartbeat",
coords: {
latitude: coords.latitude,
longitude: coords.longitude,
},
},
}; };
geolocBgLogger.debug("Syncing location to server", {
url: env.GEOLOC_SYNC_URL,
coords: payload.location.coords,
});
// Make HTTP request
const response = await fetch(env.GEOLOC_SYNC_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${userToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const responseData = await response.json();
if (responseData.ok !== true) {
throw new Error(`API returned ok: ${responseData.ok}`);
}
geolocBgLogger.info("iOS location sync completed successfully", {
status: response.status,
coords: payload.location.coords,
});
} catch (error) { } catch (error) {
// Return error result for BackgroundFetch geolocBgLogger.error("iOS location sync failed", {
return {
syncPerformed,
syncSuccessful: false,
error: error.message, error: error.message,
}; stack: error.stack,
});
}
};
// Shared heartbeat logic - mutualized between Android and iOS
const executeSync = async () => {
if (Platform.OS === "ios") {
await executeSyncIOS();
} else if (Platform.OS === "android") {
await executeSyncAndroid();
} }
}; };
export const executeHeartbeatSync = async () => { export const executeHeartbeatSync = async () => {
@ -84,7 +160,7 @@ export const executeHeartbeatSync = async () => {
const syncResult = await Promise.race([ const syncResult = await Promise.race([
executeSync(), executeSync(),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout(() => reject(new Error("changePace timeout")), 10000), setTimeout(() => reject(new Error("changePace timeout")), 20000),
), ),
]); ]);

View file

@ -8,7 +8,7 @@ import { initEmulatorMode } from "./emulatorService";
import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores"; import { getAuthState, subscribeAuthState, permissionsActions } from "~/stores";
import setLocationState from "~/location/setLocationState"; import setLocationState from "~/location/setLocationState";
import { storeLocation } from "~/utils/location/storage"; import { storeLocation } from "~/location/storage";
import env from "~/env"; import env from "~/env";

View file

@ -21,15 +21,6 @@ export default async function notifGeolocationHeartbeatSync(data) {
heartbeatLogger.info("Triggering geolocation heartbeat sync"); heartbeatLogger.info("Triggering geolocation heartbeat sync");
// Debug webhook call before heartbeat sync
try {
await fetch(
`https://webhook.site/fc954dfe-8c1e-4efc-a75e-3f9a8917f503?source=notifGeolocationHeartbeatSync`,
);
} catch (webhookError) {
// Silently ignore webhook setup errors
}
// Execute the heartbeat sync to force location update // Execute the heartbeat sync to force location update
await executeHeartbeatSync(); await executeHeartbeatSync();

View file

@ -16,7 +16,7 @@ import {
import { deepEqual } from "fast-equals"; import { deepEqual } from "fast-equals";
import { useAlertState } from "~/stores"; import { useAlertState } from "~/stores";
import { storeLocation } from "~/utils/location/storage"; import { storeLocation } from "~/location/storage";
import useLocation from "~/hooks/useLocation"; import useLocation from "~/hooks/useLocation";
import withConnectivity from "~/hoc/withConnectivity"; import withConnectivity from "~/hoc/withConnectivity";

View file

@ -13,7 +13,7 @@ import { getDistance } from "geolib";
import { routeToInstructions } from "~/lib/geo/osrmTextInstructions"; import { routeToInstructions } from "~/lib/geo/osrmTextInstructions";
import getRouteState from "~/lib/geo/getRouteState"; import getRouteState from "~/lib/geo/getRouteState";
import shallowCompare from "~/utils/array/shallowCompare"; import shallowCompare from "~/utils/array/shallowCompare";
import { storeLocation } from "~/utils/location/storage"; import { storeLocation } from "~/location/storage";
import useLocation from "~/hooks/useLocation"; import useLocation from "~/hooks/useLocation";
import withConnectivity from "~/hoc/withConnectivity"; import withConnectivity from "~/hoc/withConnectivity";

View file

@ -30,15 +30,6 @@ export const initializeBackgroundFetch = async () => {
let syncResult = null; let syncResult = null;
try { try {
// Debug webhook call before heartbeat sync
try {
await fetch(
`https://webhook.site/fc954dfe-8c1e-4efc-a75e-3f9a8917f503?source=backgroundFetch`,
);
} catch (webhookError) {
// Silently ignore webhook setup errors
}
// Execute the shared heartbeat logic and get result // Execute the shared heartbeat logic and get result
syncResult = await executeHeartbeatSync(); syncResult = await executeHeartbeatSync();
backgroundFetchLogger.debug("Heartbeat sync completed", { backgroundFetchLogger.debug("Heartbeat sync completed", {