@umituz/react-native-location 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/application/ports/ICachePort.ts +35 -0
- package/src/application/ports/ILocationPort.ts +58 -0
- package/src/application/ports/ILoggerPort.ts +30 -0
- package/src/domain/errors/LocationErrors.ts +76 -0
- package/src/domain/services/PermissionService.ts +59 -0
- package/src/domain/value-objects/CoordinatesVO.ts +66 -0
- package/src/domain/value-objects/DistanceVO.ts +64 -0
- package/src/index.ts +33 -4
- package/src/infrastructure/repositories/LocationCache.repository.ts +68 -0
- package/src/infrastructure/services/LocationLoggerService.ts +58 -0
- package/src/infrastructure/services/LocationService.ts +98 -118
- package/src/infrastructure/services/LocationWatcherService.ts +95 -0
- package/src/infrastructure/utils/Accuracy.utils.ts +51 -0
- package/src/infrastructure/utils/ObjectComparison.utils.ts +22 -0
- package/src/presentation/hooks/useLocationHook.ts +99 -0
- package/src/presentation/hooks/useLocationWatchHook.ts +98 -0
- package/src/infrastructure/services/LocationWatcher.ts +0 -96
- package/src/infrastructure/utils/LocationUtils.ts +0 -181
- package/src/presentation/hooks/useLocation.ts +0 -74
- package/src/presentation/hooks/useLocationWatch.ts +0 -62
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Layer - Location Service
|
|
3
|
+
*
|
|
4
|
+
* Refactored location service using domain services and ports.
|
|
5
|
+
* DRY principle - no code duplication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import * as Location from "expo-location";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../../types/location.types";
|
|
10
|
-
import { LocationUtils } from "../utils/LocationUtils";
|
|
9
|
+
import { LocationData, LocationAddress, LocationConfig } from "../../types/location.types";
|
|
10
|
+
import { ILocationPort } from "../../application/ports/ILocationPort";
|
|
11
|
+
import { ICachePort } from "../../application/ports/ICachePort";
|
|
12
|
+
import { ILoggerPort } from "../../application/ports/ILoggerPort";
|
|
13
|
+
import { PermissionService } from "../../domain/services/PermissionService";
|
|
14
|
+
import { LocationErrors } from "../../domain/errors/LocationErrors";
|
|
15
|
+
import { CacheUtils } from "../repositories/LocationCache.repository";
|
|
11
16
|
|
|
12
17
|
interface CachedLocationData {
|
|
13
18
|
location: LocationData;
|
|
@@ -23,67 +28,20 @@ const DEFAULT_CONFIG: Required<LocationConfig> = {
|
|
|
23
28
|
withAddress: true,
|
|
24
29
|
};
|
|
25
30
|
|
|
26
|
-
export class LocationService {
|
|
31
|
+
export class LocationService implements ILocationPort {
|
|
27
32
|
private config: Required<LocationConfig>;
|
|
28
33
|
private inFlightRequest: Promise<LocationData> | null = null;
|
|
29
34
|
|
|
30
|
-
constructor(
|
|
35
|
+
constructor(
|
|
36
|
+
private cache: ICachePort,
|
|
37
|
+
private logger: ILoggerPort,
|
|
38
|
+
config: LocationConfig = {}
|
|
39
|
+
) {
|
|
31
40
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
32
41
|
}
|
|
33
42
|
|
|
34
|
-
async requestPermissions(): Promise<boolean> {
|
|
35
|
-
try {
|
|
36
|
-
const { status: current } = await Location.getForegroundPermissionsAsync();
|
|
37
|
-
if (current === "granted") return true;
|
|
38
|
-
|
|
39
|
-
if (__DEV__) console.log("[LocationService] Requesting permissions...");
|
|
40
|
-
const { status } = await Location.requestForegroundPermissionsAsync();
|
|
41
|
-
return status === "granted";
|
|
42
|
-
} catch (error) {
|
|
43
|
-
if (__DEV__) console.error("[LocationService] Error requesting permissions:", error);
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private getCacheKey(): string {
|
|
49
|
-
return LocationUtils.generateCacheKey(this.config.cacheKey, this.config.withAddress);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private async getCachedLocation(): Promise<LocationData | null> {
|
|
53
|
-
if (!this.config.enableCache) return null;
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const cacheKey = this.getCacheKey();
|
|
57
|
-
const result = await storageRepository.getItem<CachedLocationData | null>(cacheKey, null);
|
|
58
|
-
const cached = unwrap(result, null);
|
|
59
|
-
|
|
60
|
-
if (!cached) return null;
|
|
61
|
-
|
|
62
|
-
if (LocationUtils.isCacheExpired(cached.timestamp, this.config.cacheDuration)) {
|
|
63
|
-
await storageRepository.removeItem(cacheKey);
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (__DEV__) console.log("[LocationService] Using cached location");
|
|
68
|
-
return cached.location;
|
|
69
|
-
} catch (error) {
|
|
70
|
-
if (__DEV__) console.error("[LocationService] Cache read error:", error);
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private async cacheLocation(location: LocationData): Promise<void> {
|
|
76
|
-
if (!this.config.enableCache) return;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const data: CachedLocationData = { location, timestamp: Date.now() };
|
|
80
|
-
await storageRepository.setItem(this.getCacheKey(), data);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
if (__DEV__) console.error("[LocationService] Cache write error:", error);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
43
|
async getCurrentPosition(): Promise<LocationData> {
|
|
44
|
+
// Request deduplication
|
|
87
45
|
if (this.inFlightRequest) {
|
|
88
46
|
return this.inFlightRequest;
|
|
89
47
|
}
|
|
@@ -97,42 +55,65 @@ export class LocationService {
|
|
|
97
55
|
}
|
|
98
56
|
|
|
99
57
|
private async fetchPosition(): Promise<LocationData> {
|
|
58
|
+
// Cache check
|
|
100
59
|
const cached = await this.getCachedLocation();
|
|
101
60
|
if (cached) return cached;
|
|
102
61
|
|
|
103
|
-
|
|
62
|
+
// Permission check
|
|
63
|
+
const hasPermission = await PermissionService.request();
|
|
104
64
|
if (!hasPermission) {
|
|
105
|
-
throw
|
|
65
|
+
throw LocationErrors.permissionDenied();
|
|
106
66
|
}
|
|
107
67
|
|
|
108
|
-
|
|
109
|
-
|
|
68
|
+
// Fetch with timeout
|
|
69
|
+
const location = await this.getPositionWithTimeout();
|
|
110
70
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
71
|
+
// Optional reverse geocoding
|
|
72
|
+
let address: LocationAddress | undefined;
|
|
73
|
+
if (this.config.withAddress) {
|
|
74
|
+
address = await this.reverseGeocode(
|
|
75
|
+
location.coords.latitude,
|
|
76
|
+
location.coords.longitude
|
|
77
|
+
);
|
|
78
|
+
}
|
|
118
79
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
80
|
+
const locationData: LocationData = {
|
|
81
|
+
coords: {
|
|
82
|
+
latitude: location.coords.latitude,
|
|
83
|
+
longitude: location.coords.longitude,
|
|
84
|
+
},
|
|
85
|
+
timestamp: location.timestamp,
|
|
86
|
+
address,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await this.cacheLocation(locationData);
|
|
90
|
+
return locationData;
|
|
91
|
+
}
|
|
127
92
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
93
|
+
private async getCachedLocation(): Promise<LocationData | null> {
|
|
94
|
+
if (!this.config.enableCache) return null;
|
|
95
|
+
|
|
96
|
+
const cacheKey = CacheUtils.generateKey(this.config.cacheKey, this.config.withAddress);
|
|
97
|
+
const cached = await this.cache.get(cacheKey);
|
|
98
|
+
|
|
99
|
+
if (!cached) return null;
|
|
132
100
|
|
|
133
|
-
|
|
134
|
-
|
|
101
|
+
if (CacheUtils.isExpired(cached.timestamp, this.config.cacheDuration)) {
|
|
102
|
+
await this.cache.remove(cacheKey);
|
|
103
|
+
return null;
|
|
135
104
|
}
|
|
105
|
+
|
|
106
|
+
this.logger.debug("Using cached location");
|
|
107
|
+
return cached.location;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async cacheLocation(location: LocationData): Promise<void> {
|
|
111
|
+
if (!this.config.enableCache) return;
|
|
112
|
+
|
|
113
|
+
const cacheKey = CacheUtils.generateKey(this.config.cacheKey, this.config.withAddress);
|
|
114
|
+
const data: CachedLocationData = { location, timestamp: Date.now() };
|
|
115
|
+
|
|
116
|
+
await this.cache.set(cacheKey, data);
|
|
136
117
|
}
|
|
137
118
|
|
|
138
119
|
private async getPositionWithTimeout(): Promise<Location.LocationObject> {
|
|
@@ -144,17 +125,18 @@ export class LocationService {
|
|
|
144
125
|
|
|
145
126
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
146
127
|
timeoutId = setTimeout(() => {
|
|
147
|
-
reject(
|
|
128
|
+
reject(
|
|
129
|
+
LocationErrors.timeout(
|
|
130
|
+
`Location request timed out after ${this.config.timeout}ms`
|
|
131
|
+
)
|
|
132
|
+
);
|
|
148
133
|
}, this.config.timeout);
|
|
149
134
|
});
|
|
150
135
|
|
|
151
136
|
try {
|
|
152
|
-
|
|
153
|
-
return result;
|
|
137
|
+
return await Promise.race([locationPromise, timeoutPromise]);
|
|
154
138
|
} finally {
|
|
155
|
-
if (timeoutId !== null)
|
|
156
|
-
clearTimeout(timeoutId);
|
|
157
|
-
}
|
|
139
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
158
140
|
}
|
|
159
141
|
}
|
|
160
142
|
|
|
@@ -163,30 +145,35 @@ export class LocationService {
|
|
|
163
145
|
const [address] = await Location.reverseGeocodeAsync({ latitude, longitude });
|
|
164
146
|
if (!address) return undefined;
|
|
165
147
|
|
|
166
|
-
return
|
|
148
|
+
return this.buildAddress(address);
|
|
167
149
|
} catch (error) {
|
|
168
|
-
|
|
150
|
+
this.logger.error("Reverse geocode failed", error);
|
|
169
151
|
return undefined;
|
|
170
152
|
}
|
|
171
153
|
}
|
|
172
154
|
|
|
155
|
+
private buildAddress(
|
|
156
|
+
address: Pick<Location.LocationGeocodedAddress, "street" | "city" | "region" | "country">
|
|
157
|
+
): LocationAddress {
|
|
158
|
+
const parts = [address.street, address.city, address.region, address.country].filter(
|
|
159
|
+
Boolean
|
|
160
|
+
) as string[];
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
city: address.city ?? null,
|
|
164
|
+
region: address.region ?? null,
|
|
165
|
+
country: address.country ?? null,
|
|
166
|
+
street: address.street ?? null,
|
|
167
|
+
formattedAddress: parts.length > 0 ? parts.join(", ") : null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
173
171
|
async isLocationEnabled(): Promise<boolean> {
|
|
174
|
-
|
|
175
|
-
return await Location.hasServicesEnabledAsync();
|
|
176
|
-
} catch (error) {
|
|
177
|
-
if (__DEV__) console.error("[LocationService] Error checking location enabled:", error);
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
172
|
+
return PermissionService.areServicesEnabled();
|
|
180
173
|
}
|
|
181
174
|
|
|
182
|
-
async getPermissionStatus(): Promise<
|
|
183
|
-
|
|
184
|
-
const { status } = await Location.getForegroundPermissionsAsync();
|
|
185
|
-
return status;
|
|
186
|
-
} catch (error) {
|
|
187
|
-
if (__DEV__) console.error("[LocationService] Error getting permission status:", error);
|
|
188
|
-
return Location.PermissionStatus.UNDETERMINED;
|
|
189
|
-
}
|
|
175
|
+
async getPermissionStatus(): Promise<string> {
|
|
176
|
+
return await PermissionService.getStatus();
|
|
190
177
|
}
|
|
191
178
|
|
|
192
179
|
async getLastKnownPosition(): Promise<LocationData | null> {
|
|
@@ -202,15 +189,8 @@ export class LocationService {
|
|
|
202
189
|
timestamp: location.timestamp,
|
|
203
190
|
};
|
|
204
191
|
} catch (error) {
|
|
205
|
-
|
|
192
|
+
this.logger.error("Error getting last known position", error);
|
|
206
193
|
return null;
|
|
207
194
|
}
|
|
208
195
|
}
|
|
209
|
-
|
|
210
|
-
private createError(code: LocationErrorCode, message: string): LocationError & Error {
|
|
211
|
-
const error = new Error(message) as Error & LocationError;
|
|
212
|
-
error.name = "LocationError";
|
|
213
|
-
error.code = code;
|
|
214
|
-
return error;
|
|
215
|
-
}
|
|
216
196
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Layer - Location Watcher Service
|
|
3
|
+
*
|
|
4
|
+
* Refactored watcher using domain services.
|
|
5
|
+
* Shares permission logic with LocationService (DRY).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Location from "expo-location";
|
|
9
|
+
import { LocationData, LocationWatcherOptions } from "../../types/location.types";
|
|
10
|
+
import { ILocationWatcherPort } from "../../application/ports/ILocationPort";
|
|
11
|
+
import { ILoggerPort } from "../../application/ports/ILoggerPort";
|
|
12
|
+
import { PermissionService } from "../../domain/services/PermissionService";
|
|
13
|
+
import { LocationErrors } from "../../domain/errors/LocationErrors";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OPTIONS: Required<LocationWatcherOptions> = {
|
|
16
|
+
accuracy: Location.Accuracy.Balanced,
|
|
17
|
+
distanceInterval: 10,
|
|
18
|
+
timeInterval: 10000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class LocationWatcher implements ILocationWatcherPort {
|
|
22
|
+
private subscription: Location.LocationSubscription | null = null;
|
|
23
|
+
private options: Required<LocationWatcherOptions>;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private logger: ILoggerPort,
|
|
27
|
+
options: LocationWatcherOptions = {}
|
|
28
|
+
) {
|
|
29
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async watchPosition(
|
|
33
|
+
onSuccess: (location: LocationData) => void,
|
|
34
|
+
onError?: (error: Error) => void
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
this.clearWatch();
|
|
37
|
+
|
|
38
|
+
// Permission check
|
|
39
|
+
const hasPermission = await PermissionService.request();
|
|
40
|
+
if (!hasPermission) {
|
|
41
|
+
const error = LocationErrors.permissionDenied();
|
|
42
|
+
onError?.(error);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.subscription = await Location.watchPositionAsync(
|
|
48
|
+
{
|
|
49
|
+
accuracy: this.options.accuracy,
|
|
50
|
+
distanceInterval: this.options.distanceInterval,
|
|
51
|
+
timeInterval: this.options.timeInterval,
|
|
52
|
+
},
|
|
53
|
+
(location) => {
|
|
54
|
+
onSuccess(this.mapToLocationData(location));
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
this.logger.debug("Started watching location");
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const locationError = this.handleError(error);
|
|
61
|
+
onError?.(locationError);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
clearWatch(): void {
|
|
66
|
+
if (this.subscription) {
|
|
67
|
+
this.logger.debug("Clearing location watch");
|
|
68
|
+
this.subscription.remove();
|
|
69
|
+
this.subscription = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
isWatching(): boolean {
|
|
74
|
+
return this.subscription !== null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private mapToLocationData(location: Location.LocationObject): LocationData {
|
|
78
|
+
return {
|
|
79
|
+
coords: {
|
|
80
|
+
latitude: location.coords.latitude,
|
|
81
|
+
longitude: location.coords.longitude,
|
|
82
|
+
},
|
|
83
|
+
timestamp: location.timestamp,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private handleError(error: unknown): Error {
|
|
88
|
+
this.logger.error("Error watching position", error);
|
|
89
|
+
|
|
90
|
+
const code = LocationErrors.extractErrorCode(error);
|
|
91
|
+
const message = LocationErrors.extractMessage(error);
|
|
92
|
+
|
|
93
|
+
return LocationErrors.unknown(message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Layer - Accuracy Utilities
|
|
3
|
+
*
|
|
4
|
+
* Accuracy formatting and validation utilities.
|
|
5
|
+
* Extracted from LocationUtils for single responsibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class AccuracyUtils {
|
|
9
|
+
private static readonly EXCELLENT_THRESHOLD = 10;
|
|
10
|
+
private static readonly GOOD_THRESHOLD = 50;
|
|
11
|
+
private static readonly FAIR_THRESHOLD = 100;
|
|
12
|
+
private static readonly DEFAULT_MAX_ACCURACY = 100;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Accuracy değerini formatla
|
|
16
|
+
*/
|
|
17
|
+
static format(accuracy: number | null | undefined): string {
|
|
18
|
+
if (accuracy === null || accuracy === undefined) {
|
|
19
|
+
return "Unknown";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (accuracy < this.EXCELLENT_THRESHOLD) {
|
|
23
|
+
return `Excellent (±${Math.round(accuracy)}m)`;
|
|
24
|
+
}
|
|
25
|
+
if (accuracy < this.GOOD_THRESHOLD) {
|
|
26
|
+
return `Good (±${Math.round(accuracy)}m)`;
|
|
27
|
+
}
|
|
28
|
+
if (accuracy < this.FAIR_THRESHOLD) {
|
|
29
|
+
return `Fair (±${Math.round(accuracy)}m)`;
|
|
30
|
+
}
|
|
31
|
+
return `Poor (±${Math.round(accuracy)}m)`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Accuracy değeri geçerli mi?
|
|
36
|
+
*/
|
|
37
|
+
static isValid(
|
|
38
|
+
accuracy: number | null | undefined,
|
|
39
|
+
maxAccuracy = this.DEFAULT_MAX_ACCURACY
|
|
40
|
+
): boolean {
|
|
41
|
+
if (accuracy === null || accuracy === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!Number.isFinite(accuracy)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return accuracy > 0 && accuracy <= maxAccuracy;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Layer - Object Comparison Utilities
|
|
3
|
+
*
|
|
4
|
+
* Deep comparison utilities for React hooks.
|
|
5
|
+
* Used for stable reference generation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class ObjectComparison {
|
|
9
|
+
/**
|
|
10
|
+
* İki objeyi derinlemesine karşılaştır
|
|
11
|
+
*/
|
|
12
|
+
static deepEqual(obj1: unknown, obj2: unknown): boolean {
|
|
13
|
+
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stable string referans oluştur
|
|
18
|
+
*/
|
|
19
|
+
static getStableReference(obj: unknown): string {
|
|
20
|
+
return JSON.stringify(obj ?? {});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Layer - useLocation Hook
|
|
3
|
+
*
|
|
4
|
+
* Refactored using domain errors and ports.
|
|
5
|
+
* Simplified error handling using LocationErrors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
9
|
+
import { LocationService } from "../../infrastructure/services/LocationService";
|
|
10
|
+
import { LocationCacheRepository } from "../../infrastructure/repositories/LocationCache.repository";
|
|
11
|
+
import { LocationLogger } from "../../infrastructure/services/LocationLoggerService";
|
|
12
|
+
import { LocationData, LocationError, LocationConfig } from "../../types/location.types";
|
|
13
|
+
import { LocationErrors } from "../../domain/errors/LocationErrors";
|
|
14
|
+
import { ILoggerPort } from "../../application/ports/ILoggerPort";
|
|
15
|
+
|
|
16
|
+
// Default logger instance
|
|
17
|
+
const defaultLogger: ILoggerPort = new LocationLogger("useLocation");
|
|
18
|
+
|
|
19
|
+
export interface UseLocationResult {
|
|
20
|
+
location: LocationData | null;
|
|
21
|
+
isLoading: boolean;
|
|
22
|
+
error: LocationError | null;
|
|
23
|
+
getCurrentLocation: () => Promise<LocationData | null>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Stable reference generator for config comparison
|
|
28
|
+
*/
|
|
29
|
+
function getStableReference(obj: unknown): string {
|
|
30
|
+
return JSON.stringify(obj ?? {});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useLocation(
|
|
34
|
+
config?: LocationConfig,
|
|
35
|
+
logger: ILoggerPort = defaultLogger
|
|
36
|
+
): UseLocationResult {
|
|
37
|
+
// Stable references for config and service
|
|
38
|
+
const configStringRef = useRef<string>(getStableReference(config));
|
|
39
|
+
const serviceRef = useRef<LocationService | null>(null);
|
|
40
|
+
|
|
41
|
+
// Update service when config changes
|
|
42
|
+
const currentConfigString = getStableReference(config);
|
|
43
|
+
if (configStringRef.current !== currentConfigString) {
|
|
44
|
+
configStringRef.current = currentConfigString;
|
|
45
|
+
serviceRef.current = new LocationService(
|
|
46
|
+
new LocationCacheRepository(),
|
|
47
|
+
logger,
|
|
48
|
+
config
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Initialize service on first render
|
|
53
|
+
if (!serviceRef.current) {
|
|
54
|
+
serviceRef.current = new LocationService(new LocationCacheRepository(), logger, config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const [location, setLocation] = useState<LocationData | null>(null);
|
|
58
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
59
|
+
const [error, setError] = useState<LocationError | null>(null);
|
|
60
|
+
const mountedRef = useRef(true);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
mountedRef.current = true;
|
|
64
|
+
return () => {
|
|
65
|
+
mountedRef.current = false;
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const getCurrentLocation = useCallback(async () => {
|
|
70
|
+
if (!serviceRef.current) return null;
|
|
71
|
+
|
|
72
|
+
setIsLoading(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const data = await serviceRef.current.getCurrentPosition();
|
|
77
|
+
if (mountedRef.current) {
|
|
78
|
+
setLocation(data);
|
|
79
|
+
}
|
|
80
|
+
return data;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const locationError: LocationError = {
|
|
83
|
+
code: LocationErrors.extractErrorCode(err),
|
|
84
|
+
message: LocationErrors.extractMessage(err),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (mountedRef.current) {
|
|
88
|
+
setError(locationError);
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
} finally {
|
|
92
|
+
if (mountedRef.current) {
|
|
93
|
+
setIsLoading(false);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [configStringRef]);
|
|
97
|
+
|
|
98
|
+
return { location, isLoading, error, getCurrentLocation };
|
|
99
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Layer - useLocationWatch Hook
|
|
3
|
+
*
|
|
4
|
+
* Refactored using LocationWatcher and domain services.
|
|
5
|
+
* Simplified state management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
9
|
+
import { LocationWatcher } from "../../infrastructure/services/LocationWatcherService";
|
|
10
|
+
import { LocationLogger } from "../../infrastructure/services/LocationLoggerService";
|
|
11
|
+
import { LocationData, LocationError, LocationWatcherOptions } from "../../types/location.types";
|
|
12
|
+
import { ILoggerPort } from "../../application/ports/ILoggerPort";
|
|
13
|
+
import { LocationErrors } from "../../domain/errors/LocationErrors";
|
|
14
|
+
|
|
15
|
+
// Default logger instance
|
|
16
|
+
const defaultLogger: ILoggerPort = new LocationLogger("useLocationWatch");
|
|
17
|
+
|
|
18
|
+
export interface UseLocationWatchResult {
|
|
19
|
+
location: LocationData | null;
|
|
20
|
+
error: LocationError | null;
|
|
21
|
+
isWatching: boolean;
|
|
22
|
+
startWatching: () => Promise<void>;
|
|
23
|
+
stopWatching: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Stable reference generator for options comparison
|
|
28
|
+
*/
|
|
29
|
+
function getStableReference(obj: unknown): string {
|
|
30
|
+
return JSON.stringify(obj ?? {});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useLocationWatch(
|
|
34
|
+
options?: LocationWatcherOptions,
|
|
35
|
+
logger: ILoggerPort = defaultLogger
|
|
36
|
+
): UseLocationWatchResult {
|
|
37
|
+
// Stable reference for options
|
|
38
|
+
const optionsStringRef = useRef<string>(getStableReference(options));
|
|
39
|
+
const watcherRef = useRef<LocationWatcher | null>(null);
|
|
40
|
+
|
|
41
|
+
// State
|
|
42
|
+
const [location, setLocation] = useState<LocationData | null>(null);
|
|
43
|
+
const [error, setError] = useState<LocationError | null>(null);
|
|
44
|
+
const [isWatching, setIsWatching] = useState(false);
|
|
45
|
+
|
|
46
|
+
// Initialize watcher on first render
|
|
47
|
+
if (!watcherRef.current) {
|
|
48
|
+
watcherRef.current = new LocationWatcher(logger, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const stopWatching = useCallback(() => {
|
|
52
|
+
if (watcherRef.current) {
|
|
53
|
+
watcherRef.current.clearWatch();
|
|
54
|
+
setIsWatching(false);
|
|
55
|
+
}
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const startWatching = useCallback(async () => {
|
|
59
|
+
if (!watcherRef.current) return;
|
|
60
|
+
|
|
61
|
+
stopWatching();
|
|
62
|
+
setError(null);
|
|
63
|
+
|
|
64
|
+
// Recreate watcher if options changed
|
|
65
|
+
const currentOptionsString = getStableReference(options);
|
|
66
|
+
if (optionsStringRef.current !== currentOptionsString) {
|
|
67
|
+
optionsStringRef.current = currentOptionsString;
|
|
68
|
+
watcherRef.current = new LocationWatcher(logger, options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await watcherRef.current.watchPosition(
|
|
72
|
+
(data) => {
|
|
73
|
+
setLocation(data);
|
|
74
|
+
setError(null);
|
|
75
|
+
},
|
|
76
|
+
(err) => {
|
|
77
|
+
const locationError: LocationError = {
|
|
78
|
+
code: LocationErrors.extractErrorCode(err),
|
|
79
|
+
message: LocationErrors.extractMessage(err),
|
|
80
|
+
};
|
|
81
|
+
setError(locationError);
|
|
82
|
+
setIsWatching(false);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (watcherRef.current.isWatching()) {
|
|
87
|
+
setIsWatching(true);
|
|
88
|
+
}
|
|
89
|
+
}, [optionsStringRef, stopWatching, logger, options]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
return () => {
|
|
93
|
+
stopWatching();
|
|
94
|
+
};
|
|
95
|
+
}, [stopWatching]);
|
|
96
|
+
|
|
97
|
+
return { location, error, isWatching, startWatching, stopWatching };
|
|
98
|
+
}
|