web.ts (4731B)
1 import type { IClientGeolocationPosition } from "@radroots/geo"; 2 import { err_msg, type ResolveErrorMsg } from "@radroots/utils"; 3 import { cl_geolocation_error, type ClientGeolocationErrorMessage } from "./error.js"; 4 import type { IClientGeolocation } from "./types.js"; 5 6 type GeoPolicyAllows = boolean | "unknown"; 7 type GeoPermissionState = PermissionState | "unknown"; 8 9 interface PermissionsPolicyLike { 10 allowsFeature(feature: "geolocation"): boolean; 11 } 12 13 interface DocumentWithPermissionsPolicy extends Document { 14 permissionsPolicy: PermissionsPolicyLike; 15 } 16 17 interface NavigatorWithPermissions extends Navigator { 18 permissions: Permissions; 19 } 20 21 interface GeoDebug { 22 policy_allows: GeoPolicyAllows; 23 permission_state: GeoPermissionState; 24 error_code?: number; 25 error_message?: string; 26 user_agent: string; 27 } 28 29 const geo_debug_enabled = true; 30 31 function has_permissions_policy(doc: Document): doc is DocumentWithPermissionsPolicy { 32 return "permissionsPolicy" in doc; 33 } 34 35 function has_permissions_api(nav: Navigator): nav is NavigatorWithPermissions { 36 return "permissions" in nav; 37 } 38 39 function read_policy_allows_geolocation(doc: Document): GeoPolicyAllows { 40 if (!has_permissions_policy(doc)) return "unknown"; 41 try { 42 return doc.permissionsPolicy.allowsFeature("geolocation"); 43 } catch { 44 return "unknown"; 45 } 46 } 47 48 async function read_permission_state_geolocation(nav: Navigator): Promise<GeoPermissionState> { 49 if (!has_permissions_api(nav)) return "unknown"; 50 try { 51 const status = await nav.permissions.query({ name: "geolocation" }); 52 return status.state; 53 } catch { 54 return "unknown"; 55 } 56 } 57 58 function create_debug( 59 policy_allows: GeoPolicyAllows, 60 permission_state: GeoPermissionState 61 ): GeoDebug { 62 return { 63 policy_allows, 64 permission_state, 65 user_agent: navigator.userAgent 66 }; 67 } 68 69 function log_geo_debug(event: string, debug: GeoDebug): void { 70 if (!geo_debug_enabled) return; 71 console.debug(event, debug); 72 } 73 74 function get_current_position(): Promise<GeolocationPosition> { 75 return new Promise<GeolocationPosition>((resolve, reject) => { 76 navigator.geolocation.getCurrentPosition(resolve, reject, { 77 enableHighAccuracy: true, 78 timeout: 10000, 79 maximumAge: 30000 80 }); 81 }); 82 } 83 84 function map_error_key( 85 debug: GeoDebug, 86 error: GeolocationPositionError 87 ) { 88 if (error.code === 1) { 89 if (debug.policy_allows === false) { 90 return cl_geolocation_error.blocked_by_permissions_policy; 91 } 92 return cl_geolocation_error.permission_denied; 93 } 94 95 if (error.code === 2) { 96 return cl_geolocation_error.position_unavailable; 97 } 98 99 if (error.code === 3) { 100 return cl_geolocation_error.timeout; 101 } 102 103 return cl_geolocation_error.unknown_error; 104 } 105 106 export interface IWebGeolocation extends IClientGeolocation {} 107 108 export class WebGeolocation implements IWebGeolocation { 109 public async current(): Promise<ResolveErrorMsg<IClientGeolocationPosition, ClientGeolocationErrorMessage>> { 110 if (typeof navigator === "undefined" || typeof document === "undefined") return err_msg(cl_geolocation_error.location_unavailable); 111 if (!navigator.geolocation) return err_msg(cl_geolocation_error.location_unavailable); 112 113 const policy_allows = read_policy_allows_geolocation(document); 114 const permission_state = await read_permission_state_geolocation(navigator); 115 116 const base_debug = create_debug(policy_allows, permission_state); 117 const has_geo_error = typeof GeolocationPositionError !== "undefined"; 118 119 if (policy_allows === false) { 120 log_geo_debug("[geolocation] blocked_by_policy", base_debug); 121 return err_msg(cl_geolocation_error.blocked_by_permissions_policy); 122 } 123 124 try { 125 const position = await get_current_position(); 126 127 return { 128 lat: position.coords.latitude, 129 lng: position.coords.longitude, 130 accuracy: position.coords.accuracy 131 }; 132 } catch (e) { 133 if (has_geo_error && e instanceof GeolocationPositionError) { 134 const debug: GeoDebug = { 135 ...base_debug, 136 error_code: e.code, 137 error_message: e.message 138 }; 139 140 const key = map_error_key(debug, e); 141 log_geo_debug("[geolocation] error", debug); 142 143 return err_msg(key); 144 } 145 146 log_geo_debug("[geolocation] unknown_exception", base_debug); 147 return err_msg(cl_geolocation_error.unknown_error); 148 } 149 } 150 }