Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
open-pull-requests-limit: 0
schedule:
interval: "weekly"
36 changes: 19 additions & 17 deletions .github/workflows/react-native-cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,30 +276,32 @@ jobs:
run: |
firebase appdistribution:distribute ./ResgridUnit-ios-adhoc.ipa --app ${{ secrets.FIREBASE_IOS_APP_ID }} --groups "testers"

- name: 📋 Extract Release Notes from PR Body
- name: 📋 Prepare Release Notes file
if: ${{ matrix.platform == 'android' }}
env:
RELEASE_NOTES_INPUT: ${{ github.event.inputs.release_notes }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -eo pipefail
# Grab lines after "## Release Notes" until the next header
RELEASE_NOTES="$(printf '%s\n' "$PR_BODY" \
| awk 'f && /^## /{f=0} /^## Release Notes/{f=1; next} f')"
# Use a unique delimiter to write multiline into GITHUB_ENV
delimiter="EOF_$(date +%s)_$RANDOM"
{
echo "RELEASE_NOTES<<$delimiter"
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
echo "$delimiter"
} >> "$GITHUB_ENV"

- name: 📋 Prepare Release Notes file
if: ${{ matrix.platform == 'android' }}
run: |
# Determine source of release notes: workflow input, PR body, or recent commits
if [ -n "$RELEASE_NOTES_INPUT" ]; then
NOTES="$RELEASE_NOTES_INPUT"
elif [ -n "$PR_BODY" ]; then
NOTES="$(printf '%s\n' "$PR_BODY" \
| awk 'f && /^## /{exit} /^## Release Notes/{f=1; next} f')"
else
NOTES="$(git log -n 5 --pretty=format:'- %s')"
fi
# Fail if no notes extracted
if [ -z "$NOTES" ]; then
echo "Error: No release notes extracted" >&2
exit 1
fi
# Write header and notes to file
{
echo "## Version 7.${{ github.run_number }} - $(date +%Y-%m-%d)"
echo "## Version 10.${{ github.run_number }} - $(date +%Y-%m-%d)"
echo
printf '%s\n' "${RELEASE_NOTES:-No release notes provided.}"
printf '%s\n' "$NOTES"
} > RELEASE_NOTES.md

- name: 📦 Create Release
Expand Down
1 change: 1 addition & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
allowAlert: true,
allowBadge: true,
allowSound: true,
allowCriticalAlerts: true,
},
},
sounds: [
Expand Down
83 changes: 83 additions & 0 deletions docs/signalr-reconnect-self-blocking-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# SignalR Service Reconnection Self-Blocking Fix

## Problem

The SignalR service had a self-blocking issue where reconnection attempts would prevent direct connection attempts. Specifically:

1. When a hub was in "reconnecting" state, direct connection attempts would be blocked
2. This could lead to scenarios where:
- A reconnection attempt was in progress
- A user tried to manually connect
- The manual connection would be rejected
- If the reconnection failed, the hub would be stuck in reconnecting state
- Future manual connection attempts would continue to be blocked

## Root Cause

The service used a single `reconnectingHubs` Set to track both:
- Automatic reconnection attempts
- Direct connection attempts

This caused the guard logic in `_connectToHubInternal` (lines 240-246) to block direct connections when hubs were in reconnecting state.

## Solution

Implemented a more granular state management system:

### 1. New State Enum

```typescript
export enum HubConnectingState {
IDLE = 'idle',
RECONNECTING = 'reconnecting',
DIRECT_CONNECTING = 'direct-connecting',
}
```

### 2. State Management

- Added `hubStates: Map<string, HubConnectingState>` to track individual hub states
- Added `setHubState()` method to manage state transitions and maintain backward compatibility
- Added helper methods: `isHubConnecting()`, `isHubReconnecting()`

### 3. Updated Connection Logic

**Direct Connections (`_connectToHubInternal` and `_connectToHubWithEventingUrlInternal`):**
- Only block duplicate direct connections (same `DIRECT_CONNECTING` state)
- Allow direct connections even when hub is in `RECONNECTING` state
- Log reconnecting state but proceed with connection attempt
- Set state to `DIRECT_CONNECTING` during connection attempt
- Clean up state on both success and failure

**Automatic Reconnections:**
- Set state to `RECONNECTING` during reconnection attempts
- Clean up state on both success and failure
- Maintain existing reconnection logic and limits

### 4. Backward Compatibility

- Maintained the `reconnectingHubs` Set for existing API compatibility
- `setHubState()` automatically manages the legacy set alongside the new state map
- All existing methods continue to work as expected

## Key Changes

1. **Lines 240-246**: Changed from blocking all connections during reconnect to only blocking duplicate direct connections
2. **State Management**: Added proper state tracking with cleanup in success/failure paths
3. **Connection Isolation**: Reconnection attempts and direct connections now operate independently
4. **Cleanup**: Ensured state cleanup happens in all code paths to prevent stuck states

## Testing

- Updated existing tests to use new state management system
- All existing tests continue to pass
- Tests verify that direct connections are allowed during reconnection
- Tests verify proper state cleanup in success and failure scenarios

## Benefits

- Eliminates self-blocking behavior during reconnections
- Allows users to manually retry connections even during automatic reconnection
- Prevents permanent stuck states
- Maintains full backward compatibility
- Provides better separation of concerns between automatic and manual connections
132 changes: 132 additions & 0 deletions docs/signalr-service-refactoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# SignalR Service and Map Hook Refactoring

## Summary of Changes

This refactoring addresses multiple issues with the SignalR service and map updates to prevent concurrent API calls, improve performance, and ensure thread safety.

## Key Issues Addressed

1. **Multiple concurrent calls to GetMapDataAndMarkers**: SignalR events were triggering multiple simultaneous API calls
2. **Lack of singleton enforcement**: SignalR service singleton pattern wasn't thread-safe
3. **No request cancellation**: In-flight requests weren't being cancelled when new events came in
4. **No debouncing**: Rapid consecutive SignalR events caused unnecessary API calls
5. **No connection locking**: Multiple concurrent connection attempts to the same hub were possible

## Changes Made

### 1. Enhanced SignalR Service (`src/services/signalr.service.ts`)

#### Thread-Safe Singleton Pattern
- Added proper singleton instance management with race condition protection
- Added `resetInstance()` method for testing purposes
- Improved singleton creation with polling mechanism to prevent multiple instances

Comment on lines +19 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align docs with the singleton implementation (remove “polling” language).

Code should not use a blocking polling loop for singleton creation. Update this section to describe simple lazy initialization.

🧰 Tools
🪛 LanguageTool

[grammar] ~19-~19: There might be a mistake here.
Context: ...ts`) #### Thread-Safe Singleton Pattern - Added proper singleton instance manageme...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/signalr-service-refactoring.md around lines 19 to 23, the section
currently says the singleton uses a "polling mechanism" to prevent multiple
instances; change the wording to describe simple lazy initialization instead:
remove any mention of polling or blocking loops, state that the singleton uses a
thread-safe lazy initialization (e.g., double-checked locking or equivalent
lock/volatile pattern) to prevent race conditions, keep mention of
resetInstance() for testing, and clarify that instance creation is non-blocking
and coordinated via a lock rather than by polling.

#### Connection Locking
- Added `connectionLocks` Map to prevent concurrent connections to the same hub
- Added locking for `connectToHubWithEventingUrl()` and `connectToHub()` methods
- Added waiting logic for `disconnectFromHub()` and `invoke()` methods to wait for ongoing connections

#### Improved Reconnection Logic
- Enhanced `handleConnectionClose()` with better error handling and logging
- Added proper cleanup on max reconnection attempts reached
- Improved connection state management during reconnection attempts
- Added check to prevent reconnection if connection was re-established during delay

#### Better Error Handling
- Enhanced logging for all connection states
- Improved error context in log messages
- Added proper cleanup on connection failures

### 2. Refactored Map Hook (`src/hooks/use-map-signalr-updates.ts`)

#### Debouncing
- Added 1-second debounce delay to prevent rapid consecutive API calls
- Uses `setTimeout` to debounce SignalR update events

#### Concurrency Prevention
- Added `isUpdating` ref to prevent multiple concurrent API calls
- Only one `getMapDataAndMarkers` call can be active at a time

#### Request Cancellation
- Added `AbortController` support to cancel in-flight requests
- Previous requests are automatically cancelled when new updates come in
- Proper cleanup of abort controllers

#### Enhanced Error Handling
- Added special handling for `AbortError` (logged as debug, not error)
- Improved error context in log messages
- Better error recovery mechanisms

#### Proper Cleanup
- Added cleanup for debounce timers on unmount
- Added cleanup for abort controllers on unmount
- Proper cleanup in useEffect dependency arrays

## Performance Improvements

1. **Reduced API Calls**: Debouncing prevents excessive API calls during rapid SignalR events
2. **Request Cancellation**: Prevents unnecessary processing of outdated requests
3. **Singleton Enforcement**: Ensures only one SignalR service instance exists
4. **Connection Reuse**: Prevents duplicate connections to the same hub
5. **Better Memory Management**: Proper cleanup prevents memory leaks

## Testing

### New Test Coverage
- Comprehensive test suite for `useMapSignalRUpdates` hook (14 tests)
- Tests for debouncing, concurrency prevention, error handling, and cleanup
- Tests for AbortController integration
- Tests for edge cases and error scenarios

### Enhanced SignalR Service Tests
- Added tests for singleton behavior
- Added tests for connection locking
- Enhanced existing test coverage
- Added tests for improved reconnection logic

Comment on lines +73 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify referenced tests and exports exist (avoid stale docs).

Docs mention the hook tests and DEBOUNCE_DELAY. Please confirm files/exports are present.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Checking hook file and export..."
fd use-map-signalr-updates.ts src | sed -n '1p' | xargs -I{} rg -n "export const DEBOUNCE_DELAY" {}
echo "Checking hook tests..."
fd -e ts -e tsx "__tests__" src/hooks | xargs -I{} rg -n "describe\\(.*useMapSignalRUpdates" {} -n -C1 || true
echo "Checking enhanced service tests..."
fd -e ts "signalr.service.enhanced.test.ts" src/services/__tests__ -x echo {}

Length of output: 198


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "1. Locate hook file containing useMapSignalRUpdates:"
rg -l "function useMapSignalRUpdates" -t ts || true

echo "2. Verify DEBOUNCE_DELAY export in source:"
rg -n "export const DEBOUNCE_DELAY" -t ts || true

echo "3. List all hook test files:"
find src/hooks -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.test.tsx" -o -name "*.spec.tsx" \) -print || true

echo "4. Search hook tests for useMapSignalRUpdates:"
rg -n "useMapSignalRUpdates" -C1 --glob "src/hooks/**/*.{test,spec}.{ts,tsx}" || true

echo "5. List all service test files:"
find src/services -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) -print || true

echo "6. Search service tests for SignalR keywords (singleton, reconnect, connection):"
rg -nE "singleton|reconnect|connection" --glob "src/services/**/*.{test,spec}.ts" || true

Length of output: 6618


Remove stale DEBOUNCE_DELAY reference or add its export.

  • No export const DEBOUNCE_DELAY found in src/hooks/use-map-signalr-updates.ts—either export it or remove it from the docs.
  • Hook tests (src/hooks/__tests__/use-map-signalr-updates.test.ts) and enhanced service tests (src/services/__tests__/signalr.service.enhanced.test.ts) are present.
🧰 Tools
🪛 LanguageTool

[grammar] ~75-~75: There might be a mistake here.
Context: ...leaks ## Testing ### New Test Coverage - Comprehensive test suite for `useMapSign...

(QB_NEW_EN)


[grammar] ~76-~76: There might be a mistake here.
Context: ...r useMapSignalRUpdates hook (14 tests) - Tests for debouncing, concurrency preven...

(QB_NEW_EN)


[grammar] ~77-~77: There might be a mistake here.
Context: ... prevention, error handling, and cleanup - Tests for AbortController integration - ...

(QB_NEW_EN)


[grammar] ~78-~78: There might be a mistake here.
Context: ... - Tests for AbortController integration - Tests for edge cases and error scenarios...

(QB_NEW_EN)


[grammar] ~81-~81: There might be a mistake here.
Context: ...rios ### Enhanced SignalR Service Tests - Added tests for singleton behavior - Add...

(QB_NEW_EN)

🤖 Prompt for AI Agents
In docs/signalr-service-refactoring.md around lines 73 to 86, the documentation
references DEBOUNCE_DELAY which is not exported from
src/hooks/use-map-signalr-updates.ts; either remove the stale DEBOUNCE_DELAY
mention from the docs or export it from the hook file. To fix: if tests rely on
the constant, add a named export in src/hooks/use-map-signalr-updates.ts (export
const DEBOUNCE_DELAY = <current value>) and run tests; otherwise, delete the
DEBOUNCE_DELAY bullet from the docs and any README/test references to keep docs
and code consistent.

## Configuration

### Debounce Timing
- Default debounce delay: 1000ms (configurable via `DEBOUNCE_DELAY` constant)
- Can be adjusted based on performance requirements

### Reconnection Settings
- Max reconnection attempts: 5 (unchanged)
- Reconnection interval: 5000ms (unchanged)
- Enhanced with better cleanup and state management

## Backward Compatibility

All changes are backward compatible:
- Public API of SignalR service remains unchanged
- Map hook interface remains the same
- Existing functionality is preserved with performance improvements

## Usage

The refactored components work transparently with existing code:

```typescript
// SignalR service usage remains the same
const signalRService = SignalRService.getInstance();
await signalRService.connectToHubWithEventingUrl(config);

// Map hook usage remains the same
useMapSignalRUpdates(onMarkersUpdate);
```

## Benefits

1. **Improved Performance**: Fewer unnecessary API calls, better request management
2. **Better User Experience**: Faster map updates, reduced server load
3. **Enhanced Reliability**: Better error handling, improved connection management
4. **Memory Efficiency**: Proper cleanup prevents memory leaks
5. **Thread Safety**: Singleton pattern prevents race conditions
6. **Testability**: Comprehensive test coverage ensures reliability

## Future Considerations

1. **Configurable Debounce**: Could make debounce delay configurable via environment variables
2. **Request Priority**: Could implement priority system for different types of updates
3. **Caching**: Could add intelligent caching for map data
4. **Health Monitoring**: Could add connection health monitoring and reporting
25 changes: 17 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"build:production:android": "cross-env APP_ENV=production EXPO_NO_DOTENV=1 eas build --profile production --platform android ",
"build:internal:ios": "cross-env APP_ENV=internal EXPO_NO_DOTENV=1 eas build --profile internal --platform ios",
"build:internal:android": "cross-env APP_ENV=internal EXPO_NO_DOTENV=1 eas build --profile internal --platform android",
"postinstall": "patch-package",
"app-release": "cross-env SKIP_BRANCH_PROTECTION=true np --no-publish --no-cleanup --no-release-draft",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"type-check": "tsc --noemit",
Expand Down Expand Up @@ -91,7 +92,7 @@
"@notifee/react-native": "^9.1.8",
"@novu/react-native": "~2.6.6",
"@react-native-community/netinfo": "^11.4.1",
"@rnmapbox/maps": "10.1.38",
"@rnmapbox/maps": "10.1.42-rc.0",
"@semantic-release/git": "^10.0.1",
"@sentry/react-native": "~6.10.0",
"@shopify/flash-list": "1.7.3",
Expand Down Expand Up @@ -142,23 +143,23 @@
"react-error-boundary": "~4.0.13",
"react-hook-form": "~7.53.0",
"react-i18next": "~15.0.1",
"react-native": "0.76.9",
"react-native": "0.77.3",
"react-native-base64": "~0.2.1",
"react-native-ble-manager": "^12.1.5",
"react-native-callkeep": "github:Irfanwani/react-native-callkeep#957193d0716f1c2dfdc18e627cbff0f8a0800971",
"react-native-edge-to-edge": "~1.1.2",
"react-native-flash-message": "~0.4.2",
"react-native-gesture-handler": "~2.20.2",
"react-native-gesture-handler": "~2.22.0",
"react-native-keyboard-controller": "~1.15.2",
"react-native-logs": "~5.3.0",
"react-native-mmkv": "~3.1.0",
"react-native-reanimated": "~3.16.1",
"react-native-reanimated": "~3.16.7",
"react-native-restart": "0.0.27",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-safe-area-context": "~5.1.0",
"react-native-screens": "~4.8.0",
"react-native-svg": "~15.8.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"react-native-webview": "~13.13.1",
"react-query-kit": "~3.3.0",
"tailwind-variants": "~0.2.1",
"zod": "~3.23.8",
Expand Down Expand Up @@ -201,6 +202,8 @@
"jest-junit": "~16.0.0",
"lint-staged": "~15.2.9",
"np": "~10.0.7",
"patch-package": "^8.0.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "~3.3.3",
"react-native-svg-transformer": "~1.5.1",
"tailwindcss": "3.4.4",
Expand All @@ -225,7 +228,13 @@
},
"install": {
"exclude": [
"eslint-config-expo"
"eslint-config-expo",
"react-native@~0.76.6",
"react-native-reanimated@~3.16.1",
"react-native-gesture-handler@~2.20.0",
"react-native-screens@~4.4.0",
"react-native-safe-area-context@~4.12.0",
"react-native-webview@~13.12.5"
]
}
},
Expand Down
8 changes: 4 additions & 4 deletions src/api/common/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export const api = axiosInstance;
// Helper function to create API endpoints
export const createApiEndpoint = (endpoint: string) => {
return {
get: <T,>(params?: Record<string, unknown>) => api.get<T>(endpoint, { params }),
post: <T,>(data: Record<string, unknown>) => api.post<T>(endpoint, data),
put: <T,>(data: Record<string, unknown>) => api.put<T>(endpoint, data),
delete: <T,>(params?: Record<string, unknown>) => api.delete<T>(endpoint, { params }),
get: <T,>(params?: Record<string, unknown>, signal?: AbortSignal) => api.get<T>(endpoint, { params, signal }),
post: <T,>(data: Record<string, unknown>, signal?: AbortSignal) => api.post<T>(endpoint, data, { signal }),
put: <T,>(data: Record<string, unknown>, signal?: AbortSignal) => api.put<T>(endpoint, data, { signal }),
delete: <T,>(params?: Record<string, unknown>, signal?: AbortSignal) => api.delete<T>(endpoint, { params, signal }),
};
};
15 changes: 9 additions & 6 deletions src/api/mapping/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ const getMayLayersApi = createApiEndpoint('/Mapping/GetMayLayers');

const getMapDataAndMarkersApi = createApiEndpoint('/Mapping/GetMapDataAndMarkers');

export const getMapDataAndMarkers = async () => {
const response = await getMapDataAndMarkersApi.get<GetMapDataAndMarkersResult>();
export const getMapDataAndMarkers = async (signal?: AbortSignal) => {
const response = await getMapDataAndMarkersApi.get<GetMapDataAndMarkersResult>(undefined, signal);
return response.data;
};

export const getMayLayers = async (type: number) => {
const response = await getMayLayersApi.get<GetMapLayersResult>({
type: encodeURIComponent(type),
});
export const getMayLayers = async (type: number, signal?: AbortSignal) => {
const response = await getMayLayersApi.get<GetMapLayersResult>(
{
type: encodeURIComponent(type),
},
signal
);
return response.data;
};
Loading
Loading