1import { 2 SessionRealtimeService 3} from "./chunk-JVOIRLQM.js"; 4import { 5 BehaviorSubject, 6 Injectable, 7 NgZone, 8 Subject, 9 Subscription, 10 inject, 11 setClassMetadata, 12 ɵɵdefineInjectable 13} from "./chunk-6GJQIPT2.js"; 14import { 15 __async 16} from "./chunk-RK4NW2NR.js"; 17
18// src/app/services/network-status.service.ts 19var NetworkStatusService = class _NetworkStatusService { 20 ngZone = inject(NgZone); 21 realtimeService = inject(SessionRealtimeService); 22 statusSubject = new BehaviorSubject(typeof navigator !== "undefined" && !navigator.onLine ? "offline" : "online"); 23 /** Public observable for components to bind to */ 24 status$ = this.statusSubject.asObservable(); 25 reconnectedSubject = new Subject(); 26 /** Emitted whenever connection transitions from offline/reconnecting back to online. Used by components to re-sync stale data. */ 27 reconnected$ = this.reconnectedSubject.asObservable(); 28 restoredTimeout = null; 29 isProbing = false; 30 wasEverOffline = typeof navigator !== "undefined" && !navigator.onLine; 31 subscriptions = new Subscription(); 32 onlineHandler = () => this.handleOnlineEvent(); 33 offlineHandler = () => this.handleOfflineEvent(); 34 constructor() { 35 this.registerBrowserListeners(); 36 this.subscribeToRealtimeHub(); 37 } 38 /** Current state snapshot */ 39 get currentStatus() { 40 return this.statusSubject.value; 41 } 42 /** True if the user is considered offline or currently reconnecting */ 43 get isOffline() { 44 if (typeof navigator !== "undefined" && !navigator.onLine) { 45 return true; 46 } 47 const s = this.statusSubject.value; 48 return s === "offline" || s === "reconnecting"; 49 } 50 registerBrowserListeners() { 51 if (typeof window === "undefined") 52 return; 53 window.addEventListener("online", this.onlineHandler); 54 window.addEventListener("offline", this.offlineHandler); 55 } 56 subscribeToRealtimeHub() { 57 this.subscriptions.add(this.realtimeService.status$.subscribe((hubStatus) => { 58 if (typeof navigator !== "undefined" && !navigator.onLine) { 59 return; 60 } 61 if (hubStatus === "reconnecting") { 62 if (this.statusSubject.value !== "reconnecting") { 63 this.statusSubject.next("reconnecting"); 64 } 65 } else if (hubStatus === "connected") { 66 if (this.statusSubject.value === "reconnecting" || this.statusSubject.value === "offline") { 67 this.markOnline(); 68 } 69 } 70 })); 71 } 72 handleOfflineEvent() { 73 this.ngZone.run(() => { 74 this.clearRestoredTimeout(); 75 this.wasEverOffline = true; 76 this.statusSubject.next("offline"); 77 }); 78 } 79 handleOnlineEvent() { 80 this.ngZone.run(() => { 81 this.statusSubject.next("reconnecting"); 82 this.probeReachability(); 83 }); 84 } 85 /** 86 * Reports an HTTP error from the interceptor. 87 * If forceImmediate is true (e.g. status 0, proxy disconnect, ECONNREFUSED), transitions to offline immediately. 88 * Otherwise launches a background probe. 89 */ 90 reportHttpNetworkError(forceImmediate = false) { 91 if (forceImmediate || typeof navigator !== "undefined" && !navigator.onLine) { 92 this.handleOfflineEvent(); 93 return; 94 } 95 if (!this.isProbing) { 96 this.probeReachability(); 97 } 98 } 99 /** 100 * Active probe to verify actual internet / server reachability. 101 * Uses a cache-busted request with a short timeout. 102 */ 103 probeReachability() { 104 return __async(this, null, function* () { 105 if (this.isProbing || typeof window === "undefined") { 106 return this.statusSubject.value === "online"; 107 } 108 if (typeof navigator !== "undefined" && !navigator.onLine) { 109 this.handleOfflineEvent(); 110 return false; 111 } 112 this.isProbing = true; 113 const controller = new AbortController(); 114 const timeoutId = setTimeout(() => controller.abort(), 3500); 115 try { 116 const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"; 117 let reachable = false; 118 if (isLocalhost) { 119 try { 120 yield fetch(`https://dev.wyber.ai/favicon.ico?_nc=${Date.now()}`, { 121 mode: "no-cors", 122 cache: "no-store", 123 signal: controller.signal 124 }); 125 reachable = true; 126 } catch { 127 const res = yield fetch(`https://api.ipify.org?format=json&_nc=${Date.now()}`, { 128 method: "GET", 129 cache: "no-store", 130 signal: controller.signal 131 }); 132 reachable = res.ok || res.status > 0; 133 } 134 } else { 135 const response = yield fetch(`${window.location.origin}/favicon.ico?_nc=${Date.now()}`, { 136 method: "HEAD", 137 cache: "no-store", 138 signal: controller.signal 139 }); 140 reachable = response.ok || response.status > 0 || response.type === "opaque"; 141 } 142 clearTimeout(timeoutId); 143 this.ngZone.run(() => { 144 if (reachable) { 145 this.markOnline(); 146 } else { 147 this.wasEverOffline = true; 148 this.statusSubject.next("offline"); 149 } 150 });
151 return reachable; 152 } catch { 153 clearTimeout(timeoutId); 154 this.ngZone.run(() => { 155 this.wasEverOffline = true; 156 this.statusSubject.next("offline"); 157 }); 158 return false; 159 } finally { 160 this.isProbing = false; 161 } 162 }); 163 } 164 /** 165 * Marks status as online. If previously offline, shows 'restored' for 3.5 seconds before 'online'. 166 */ 167 markOnline() { 168 this.clearRestoredTimeout(); 169 if (this.wasEverOffline) { 170 this.reconnectedSubject.next(); 171 this.statusSubject.next("restored"); 172 this.restoredTimeout = setTimeout(() => { 173 this.ngZone.run(() => { 174 this.statusSubject.next("online"); 175 this.wasEverOffline = false; 176 }); 177 }, 3500); 178 } else { 179 this.statusSubject.next("online"); 180 } 181 } 182 /** 183 * Triggered by user clicking "Reconnect now" or manual retry. 184 * Returns true if connection was successfully verified. 185 */ 186 retryConnection() { 187 return __async(this, null, function* () { 188 this.statusSubject.next("reconnecting"); 189 const isUp = yield this.probeReachability(); 190 if (!isUp) { 191 this.statusSubject.next("offline"); 192 return false; 193 } 194 return true; 195 }); 196 } 197 clearRestoredTimeout() { 198 if (this.restoredTimeout) { 199 clearTimeout(this.restoredTimeout); 200 this.restoredTimeout = null; 201 } 202 } 203 ngOnDestroy() { 204 this.clearRestoredTimeout(); 205 this.subscriptions.unsubscribe(); 206 if (typeof window !== "undefined") { 207 window.removeEventListener("online", this.onlineHandler); 208 window.removeEventListener("offline", this.offlineHandler); 209 } 210 } 211 static \u0275fac = function NetworkStatusService_Factory(__ngFactoryType__) { 212 return new (__ngFactoryType__ || _NetworkStatusService)(); 213 }; 214 static \u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _NetworkStatusService, factory: _NetworkStatusService.\u0275fac, providedIn: "root" }); 215}; 216(() => { 217 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(NetworkStatusService, [{ 218 type: Injectable, 219 args: [{ 220 providedIn: "root" 221 }] 222 }], () => [], null); 223})(); 224
225// src/app/services/toast.service.ts 226var ToastService = class _ToastService { 227 networkService = inject(NetworkStatusService); 228 ngZone = inject(NgZone); 229 toastsSubject = new BehaviorSubject([]); 230 toasts$ = this.toastsSubject.asObservable(); 231 show(title, description, variant = "success", duration = 3e3) { 232 const toast = { 233 id: `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, 234 title, 235 description, 236 variant, 237 duration 238 }; 239 const currentToasts = this.toastsSubject.value; 240 this.toastsSubject.next([...currentToasts, toast]); 241 if (duration > 0) { 242 setTimeout(() => { 243 this.ngZone.run(() => { 244 this.remove(toast.id); 245 }); 246 }, duration); 247 } 248 } 249 success(title, description, duration = 3e3) { 250 this.show(title, description, "success", duration); 251 } 252 error(title, description) { 253 if (this.networkService.isOffline) { 254 return; 255 } 256 this.show(title, description, "error"); 257 } 258 info(title, description) { 259 if (this.networkService.isOffline) { 260 return; 261 } 262 this.show(title, description, "info"); 263 } 264 warning(title, description) { 265 if (this.networkService.isOffline) { 266 return; 267 } 268 this.show(title, description, "info"); 269 } 270 remove(id) { 271 const currentToasts = this.toastsSubject.value; 272 this.toastsSubject.next(currentToasts.filter((t) => t.id !== id)); 273 } 274 clear() { 275 this.toastsSubject.next([]); 276 } 277 static \u0275fac = function ToastService_Factory(__ngFactoryType__) { 278 return new (__ngFactoryType__ || _ToastService)(); 279 }; 280 static \u0275prov = /* @__PURE__ */ \u0275\u0275defineInjectable({ token: _ToastService, factory: _ToastService.\u0275fac, providedIn: "root" }); 281}; 282(() => { 283 (typeof ngDevMode === "undefined" || ngDevMode) && setClassMetadata(ToastService, [{ 284 type: Injectable, 285 args: [{ 286 providedIn: "root" 287 }] 288 }], null, null); 289})(); 290 291export { 292 NetworkStatusService, 293 ToastService 294}; 295//# sourceMappingURL=chunk-FYFSQ3DN.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.