@angular-helpers/browser-web-apis 21.6.0 → 21.9.0

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.
@@ -1,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, DestroyRef, PLATFORM_ID, Injectable, signal, computed, isSignal, effect, ElementRef, makeEnvironmentProviders } from '@angular/core';
2
+ import { InjectionToken, Injectable, inject, DestroyRef, PLATFORM_ID, signal, computed, isSignal, effect, ElementRef, makeEnvironmentProviders } from '@angular/core';
3
3
  import { isPlatformBrowser, isPlatformServer } from '@angular/common';
4
4
  import { Observable, fromEvent, Subject, of, map as map$1 } from 'rxjs';
5
- import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
6
- import { filter, distinctUntilChanged, map } from 'rxjs/operators';
5
+ import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import { filter, map, distinctUntilChanged } from 'rxjs/operators';
7
7
  import { Router } from '@angular/router';
8
8
 
9
9
  const BROWSER_API_LOGGER = new InjectionToken('BROWSER_API_LOGGER', {
@@ -18,14 +18,180 @@ const BROWSER_API_LOGGER = new InjectionToken('BROWSER_API_LOGGER', {
18
18
  }),
19
19
  });
20
20
 
21
+ const BROWSER_CAPABILITIES = [
22
+ { id: 'permissions', label: 'Permissions API', requiresSecureContext: false },
23
+ { id: 'geolocation', label: 'Geolocation API', requiresSecureContext: true },
24
+ { id: 'clipboard', label: 'Clipboard API', requiresSecureContext: true },
25
+ { id: 'notification', label: 'Notification API', requiresSecureContext: true },
26
+ { id: 'mediaDevices', label: 'MediaDevices API', requiresSecureContext: true },
27
+ { id: 'camera', label: 'Camera API', requiresSecureContext: true },
28
+ { id: 'webWorker', label: 'Web Worker API', requiresSecureContext: false },
29
+ { id: 'regexSecurity', label: 'Regex Security', requiresSecureContext: false },
30
+ { id: 'webStorage', label: 'Web Storage', requiresSecureContext: false },
31
+ { id: 'webShare', label: 'Web Share', requiresSecureContext: true },
32
+ { id: 'battery', label: 'Battery API', requiresSecureContext: false },
33
+ { id: 'webSocket', label: 'WebSocket API', requiresSecureContext: false },
34
+ { id: 'intersectionObserver', label: 'Intersection Observer', requiresSecureContext: false },
35
+ { id: 'resizeObserver', label: 'Resize Observer', requiresSecureContext: false },
36
+ { id: 'pageVisibility', label: 'Page Visibility API', requiresSecureContext: false },
37
+ { id: 'broadcastChannel', label: 'Broadcast Channel API', requiresSecureContext: false },
38
+ { id: 'networkInformation', label: 'Network Information API', requiresSecureContext: false },
39
+ { id: 'screenWakeLock', label: 'Screen Wake Lock API', requiresSecureContext: true },
40
+ { id: 'screenOrientation', label: 'Screen Orientation API', requiresSecureContext: false },
41
+ { id: 'fullscreen', label: 'Fullscreen API', requiresSecureContext: false },
42
+ { id: 'fileSystemAccess', label: 'File System Access API', requiresSecureContext: true },
43
+ { id: 'mediaRecorder', label: 'MediaRecorder API', requiresSecureContext: true },
44
+ { id: 'serverSentEvents', label: 'Server-Sent Events', requiresSecureContext: false },
45
+ { id: 'vibration', label: 'Vibration API', requiresSecureContext: false },
46
+ { id: 'speechSynthesis', label: 'Speech Synthesis API', requiresSecureContext: false },
47
+ { id: 'mutationObserver', label: 'Mutation Observer', requiresSecureContext: false },
48
+ { id: 'performanceObserver', label: 'Performance Observer', requiresSecureContext: false },
49
+ { id: 'idleDetector', label: 'Idle Detection API', requiresSecureContext: true },
50
+ { id: 'eyeDropper', label: 'EyeDropper API', requiresSecureContext: true },
51
+ { id: 'barcodeDetector', label: 'Barcode Detection API', requiresSecureContext: true },
52
+ { id: 'webAudio', label: 'Web Audio API', requiresSecureContext: false },
53
+ { id: 'gamepad', label: 'Gamepad API', requiresSecureContext: true },
54
+ { id: 'webBluetooth', label: 'Web Bluetooth API', requiresSecureContext: true },
55
+ { id: 'webUsb', label: 'WebUSB API', requiresSecureContext: true },
56
+ { id: 'webNfc', label: 'Web NFC API', requiresSecureContext: true },
57
+ { id: 'paymentRequest', label: 'Payment Request API', requiresSecureContext: true },
58
+ { id: 'credentialManagement', label: 'Credential Management API', requiresSecureContext: true },
59
+ ];
60
+ class BrowserCapabilityService {
61
+ getCapabilities() {
62
+ return BROWSER_CAPABILITIES;
63
+ }
64
+ isSecureContext() {
65
+ return typeof window !== 'undefined' && window.isSecureContext;
66
+ }
67
+ isSupported(capability) {
68
+ switch (capability) {
69
+ case 'permissions':
70
+ return typeof navigator !== 'undefined' && 'permissions' in navigator;
71
+ case 'geolocation':
72
+ return typeof navigator !== 'undefined' && 'geolocation' in navigator;
73
+ case 'clipboard':
74
+ return typeof navigator !== 'undefined' && 'clipboard' in navigator;
75
+ case 'notification':
76
+ return typeof window !== 'undefined' && 'Notification' in window;
77
+ case 'mediaDevices':
78
+ case 'camera':
79
+ return typeof navigator !== 'undefined' && 'mediaDevices' in navigator;
80
+ case 'webWorker':
81
+ case 'regexSecurity':
82
+ return typeof Worker !== 'undefined';
83
+ case 'webStorage':
84
+ return typeof Storage !== 'undefined';
85
+ case 'webShare':
86
+ return typeof navigator !== 'undefined' && 'share' in navigator;
87
+ case 'battery':
88
+ return typeof navigator !== 'undefined' && 'getBattery' in navigator;
89
+ case 'webSocket':
90
+ return typeof WebSocket !== 'undefined';
91
+ case 'intersectionObserver':
92
+ return typeof IntersectionObserver !== 'undefined';
93
+ case 'resizeObserver':
94
+ return typeof ResizeObserver !== 'undefined';
95
+ case 'pageVisibility':
96
+ return typeof document !== 'undefined' && 'hidden' in document;
97
+ case 'broadcastChannel':
98
+ return typeof BroadcastChannel !== 'undefined';
99
+ case 'networkInformation':
100
+ return (typeof navigator !== 'undefined' &&
101
+ ('connection' in navigator || 'mozConnection' in navigator));
102
+ case 'screenWakeLock':
103
+ return typeof navigator !== 'undefined' && 'wakeLock' in navigator;
104
+ case 'screenOrientation':
105
+ return typeof screen !== 'undefined' && 'orientation' in screen;
106
+ case 'fullscreen':
107
+ return (typeof document !== 'undefined' &&
108
+ ('fullscreenEnabled' in document || 'webkitFullscreenEnabled' in document));
109
+ case 'fileSystemAccess':
110
+ return typeof window !== 'undefined' && 'showOpenFilePicker' in window;
111
+ case 'mediaRecorder':
112
+ return typeof MediaRecorder !== 'undefined';
113
+ case 'serverSentEvents':
114
+ return typeof EventSource !== 'undefined';
115
+ case 'vibration':
116
+ return typeof navigator !== 'undefined' && 'vibrate' in navigator;
117
+ case 'speechSynthesis':
118
+ return typeof window !== 'undefined' && 'speechSynthesis' in window;
119
+ case 'mutationObserver':
120
+ return typeof MutationObserver !== 'undefined';
121
+ case 'performanceObserver':
122
+ return typeof PerformanceObserver !== 'undefined';
123
+ case 'idleDetector':
124
+ return typeof window !== 'undefined' && 'IdleDetector' in window;
125
+ case 'eyeDropper':
126
+ return typeof window !== 'undefined' && 'EyeDropper' in window;
127
+ case 'barcodeDetector':
128
+ return typeof window !== 'undefined' && 'BarcodeDetector' in window;
129
+ case 'webAudio':
130
+ return typeof window !== 'undefined' && 'AudioContext' in window;
131
+ case 'gamepad':
132
+ return typeof navigator !== 'undefined' && 'getGamepads' in navigator;
133
+ case 'webBluetooth':
134
+ return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
135
+ case 'webUsb':
136
+ return typeof navigator !== 'undefined' && 'usb' in navigator;
137
+ case 'webNfc':
138
+ return typeof window !== 'undefined' && 'NDEFReader' in window;
139
+ case 'paymentRequest':
140
+ return typeof window !== 'undefined' && 'PaymentRequest' in window;
141
+ case 'credentialManagement':
142
+ return typeof navigator !== 'undefined' && 'credentials' in navigator;
143
+ default:
144
+ return false;
145
+ }
146
+ }
147
+ getAllStatuses() {
148
+ const secureContext = this.isSecureContext();
149
+ return this.getCapabilities().map((capability) => ({
150
+ id: capability.id,
151
+ label: capability.label,
152
+ supported: this.isSupported(capability.id),
153
+ secureContext,
154
+ requiresSecureContext: capability.requiresSecureContext,
155
+ }));
156
+ }
157
+ async getPermissionState(permission) {
158
+ if (typeof navigator === 'undefined' || !('permissions' in navigator)) {
159
+ return 'unknown';
160
+ }
161
+ try {
162
+ const status = await navigator.permissions.query({ name: permission });
163
+ return status.state;
164
+ }
165
+ catch {
166
+ return 'unknown';
167
+ }
168
+ }
169
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
170
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, providedIn: 'root' });
171
+ }
172
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, decorators: [{
173
+ type: Injectable,
174
+ args: [{ providedIn: 'root' }]
175
+ }] });
176
+
21
177
  /**
22
178
  * Base class for all Browser Web API services.
23
- * Provides common functionality for:
24
- * - Platform detection (browser vs server)
25
- * - Support assertion via Template Method
26
- * - Error creation with cause chaining
27
- * - Structured logging via injectable BROWSER_API_LOGGER token
28
- * - Lifecycle management with destroyRef
179
+ *
180
+ * ## Support detection contract
181
+ *
182
+ * Services follow ONE pattern (do not invent variants):
183
+ *
184
+ * - `isSupported(): boolean` public, side-effect free, SSR-safe. Default
185
+ * implementation delegates to {@link BrowserCapabilityService} when the subclass
186
+ * overrides {@link getCapabilityId} (recommended).
187
+ * - `ensureSupported(): void` — internal Template Method. Throws when called outside
188
+ * browser or when the underlying API is missing.
189
+ *
190
+ * ## Error surfacing contract
191
+ *
192
+ * - **Imperative methods** MUST call `ensureSupported()` and throw synchronously.
193
+ * - **Stream-returning methods** MUST guard with `isSupported()` and surface
194
+ * unsupported state as `Observable.error(...)` (NOT throw inline).
29
195
  *
30
196
  * Services that also need permission querying should extend
31
197
  * `PermissionAwareBrowserApiBaseService` instead.
@@ -34,30 +200,46 @@ class BrowserApiBaseService {
34
200
  destroyRef = inject(DestroyRef);
35
201
  platformId = inject(PLATFORM_ID);
36
202
  logger = inject(BROWSER_API_LOGGER);
203
+ capabilities = inject(BrowserCapabilityService);
37
204
  /**
38
- * Check if running in browser environment using Angular's platform detection.
205
+ * Optional hook for subclasses to delegate feature detection to
206
+ * {@link BrowserCapabilityService}. Returning a capability id removes the need to
207
+ * implement `isSupported()` manually and avoids drift between per-service checks
208
+ * and the centralized capability registry.
39
209
  */
210
+ getCapabilityId() {
211
+ return null;
212
+ }
213
+ /** Public, SSR-safe support check. Override only if you need extra constraints. */
214
+ isSupported() {
215
+ if (!this.isBrowserEnvironment())
216
+ return false;
217
+ const capabilityId = this.getCapabilityId();
218
+ if (capabilityId !== null) {
219
+ return this.capabilities.isSupported(capabilityId);
220
+ }
221
+ return true;
222
+ }
40
223
  isBrowserEnvironment() {
41
224
  return isPlatformBrowser(this.platformId);
42
225
  }
43
- /**
44
- * Check if running in server environment using Angular's platform detection.
45
- */
46
226
  isServerEnvironment() {
47
227
  return isPlatformServer(this.platformId);
48
228
  }
49
229
  /**
50
- * Template Method: asserts the service can run in the current environment.
51
- * Subclasses must call super.ensureSupported() and then add their own API check.
230
+ * Template Method: asserts the service can run in the current environment. Subclasses
231
+ * that need extra checks beyond capability detection MUST call `super.ensureSupported()`
232
+ * first, then add their own check.
52
233
  */
53
234
  ensureSupported() {
54
235
  if (!this.isBrowserEnvironment()) {
55
236
  throw new Error(`${this.getApiName()} API not available in server environment`);
56
237
  }
238
+ const capabilityId = this.getCapabilityId();
239
+ if (capabilityId !== null && !this.capabilities.isSupported(capabilityId)) {
240
+ throw new Error(`${this.getApiName()} API not supported in this browser`);
241
+ }
57
242
  }
58
- /**
59
- * Create an error with proper cause chaining.
60
- */
61
243
  createError(message, cause) {
62
244
  const error = new Error(message);
63
245
  if (cause !== undefined) {
@@ -65,21 +247,12 @@ class BrowserApiBaseService {
65
247
  }
66
248
  return error;
67
249
  }
68
- /**
69
- * Log an error through the injected BROWSER_API_LOGGER (default: console).
70
- */
71
250
  logError(message, error) {
72
251
  this.logger.error(`[${this.getApiName()}] ${message}`, error);
73
252
  }
74
- /**
75
- * Log a warning through the injected BROWSER_API_LOGGER (default: console).
76
- */
77
253
  logWarn(message) {
78
254
  this.logger.warn(`[${this.getApiName()}] ${message}`);
79
255
  }
80
- /**
81
- * Log an informational message through the injected BROWSER_API_LOGGER (default: console).
82
- */
83
256
  logInfo(message) {
84
257
  this.logger.info(`[${this.getApiName()}] ${message}`);
85
258
  }
@@ -112,8 +285,8 @@ class PermissionsService extends BrowserApiBaseService {
112
285
  throw error;
113
286
  }
114
287
  }
115
- isSupported() {
116
- return this.isBrowserEnvironment() && 'permissions' in navigator;
288
+ getCapabilityId() {
289
+ return 'permissions';
117
290
  }
118
291
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
119
292
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService });
@@ -127,10 +300,13 @@ class CameraService extends BrowserApiBaseService {
127
300
  getApiName() {
128
301
  return 'camera';
129
302
  }
303
+ getCapabilityId() {
304
+ return 'camera';
305
+ }
130
306
  ensureSupported() {
131
307
  super.ensureSupported();
132
308
  if (!navigator.mediaDevices?.getUserMedia) {
133
- throw new Error('Camera API not supported — a secure context (HTTPS) is required');
309
+ throw new Error('Camera API not supported — getUserMedia missing or HTTPS required');
134
310
  }
135
311
  }
136
312
  async startCamera(constraints) {
@@ -246,11 +422,8 @@ class GeolocationService extends BrowserApiBaseService {
246
422
  getApiName() {
247
423
  return 'geolocation';
248
424
  }
249
- ensureSupported() {
250
- super.ensureSupported();
251
- if (!('geolocation' in navigator)) {
252
- throw new Error('Geolocation API not supported — a secure context (HTTPS) is required');
253
- }
425
+ getCapabilityId() {
426
+ return 'geolocation';
254
427
  }
255
428
  getCurrentPosition(options) {
256
429
  this.ensureSupported();
@@ -292,6 +465,9 @@ class MediaDevicesService extends BrowserApiBaseService {
292
465
  getApiName() {
293
466
  return 'media-devices';
294
467
  }
468
+ getCapabilityId() {
469
+ return 'mediaDevices';
470
+ }
295
471
  ensureSupported() {
296
472
  super.ensureSupported();
297
473
  if (!navigator.mediaDevices) {
@@ -420,11 +596,8 @@ class NotificationService extends BrowserApiBaseService {
420
596
  getApiName() {
421
597
  return 'notifications';
422
598
  }
423
- ensureSupported() {
424
- super.ensureSupported();
425
- if (!('Notification' in window)) {
426
- throw new Error('Notifications API not supported in this browser');
427
- }
599
+ getCapabilityId() {
600
+ return 'notification';
428
601
  }
429
602
  get permission() {
430
603
  if (!this.isBrowserEnvironment() || !('Notification' in window)) {
@@ -460,6 +633,9 @@ class ClipboardService extends BrowserApiBaseService {
460
633
  getApiName() {
461
634
  return 'clipboard';
462
635
  }
636
+ getCapabilityId() {
637
+ return 'clipboard';
638
+ }
463
639
  ensureSupported() {
464
640
  super.ensureSupported();
465
641
  if (!navigator.clipboard) {
@@ -493,173 +669,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
493
669
  type: Injectable
494
670
  }] });
495
671
 
496
- const BROWSER_CAPABILITIES = [
497
- { id: 'permissions', label: 'Permissions API', requiresSecureContext: false },
498
- { id: 'geolocation', label: 'Geolocation API', requiresSecureContext: true },
499
- { id: 'clipboard', label: 'Clipboard API', requiresSecureContext: true },
500
- { id: 'notification', label: 'Notification API', requiresSecureContext: true },
501
- { id: 'mediaDevices', label: 'MediaDevices API', requiresSecureContext: true },
502
- { id: 'camera', label: 'Camera API', requiresSecureContext: true },
503
- { id: 'webWorker', label: 'Web Worker API', requiresSecureContext: false },
504
- { id: 'regexSecurity', label: 'Regex Security', requiresSecureContext: false },
505
- { id: 'webStorage', label: 'Web Storage', requiresSecureContext: false },
506
- { id: 'webShare', label: 'Web Share', requiresSecureContext: true },
507
- { id: 'battery', label: 'Battery API', requiresSecureContext: false },
508
- { id: 'webSocket', label: 'WebSocket API', requiresSecureContext: false },
509
- { id: 'intersectionObserver', label: 'Intersection Observer', requiresSecureContext: false },
510
- { id: 'resizeObserver', label: 'Resize Observer', requiresSecureContext: false },
511
- { id: 'pageVisibility', label: 'Page Visibility API', requiresSecureContext: false },
512
- { id: 'broadcastChannel', label: 'Broadcast Channel API', requiresSecureContext: false },
513
- { id: 'networkInformation', label: 'Network Information API', requiresSecureContext: false },
514
- { id: 'screenWakeLock', label: 'Screen Wake Lock API', requiresSecureContext: true },
515
- { id: 'screenOrientation', label: 'Screen Orientation API', requiresSecureContext: false },
516
- { id: 'fullscreen', label: 'Fullscreen API', requiresSecureContext: false },
517
- { id: 'fileSystemAccess', label: 'File System Access API', requiresSecureContext: true },
518
- { id: 'mediaRecorder', label: 'MediaRecorder API', requiresSecureContext: true },
519
- { id: 'serverSentEvents', label: 'Server-Sent Events', requiresSecureContext: false },
520
- { id: 'vibration', label: 'Vibration API', requiresSecureContext: false },
521
- { id: 'speechSynthesis', label: 'Speech Synthesis API', requiresSecureContext: false },
522
- { id: 'mutationObserver', label: 'Mutation Observer', requiresSecureContext: false },
523
- { id: 'performanceObserver', label: 'Performance Observer', requiresSecureContext: false },
524
- { id: 'idleDetector', label: 'Idle Detection API', requiresSecureContext: true },
525
- { id: 'eyeDropper', label: 'EyeDropper API', requiresSecureContext: true },
526
- { id: 'barcodeDetector', label: 'Barcode Detection API', requiresSecureContext: true },
527
- { id: 'webAudio', label: 'Web Audio API', requiresSecureContext: false },
528
- { id: 'gamepad', label: 'Gamepad API', requiresSecureContext: true },
529
- { id: 'webBluetooth', label: 'Web Bluetooth API', requiresSecureContext: true },
530
- { id: 'webUsb', label: 'WebUSB API', requiresSecureContext: true },
531
- { id: 'webNfc', label: 'Web NFC API', requiresSecureContext: true },
532
- { id: 'paymentRequest', label: 'Payment Request API', requiresSecureContext: true },
533
- { id: 'credentialManagement', label: 'Credential Management API', requiresSecureContext: true },
534
- ];
535
- class BrowserCapabilityService {
536
- getCapabilities() {
537
- return BROWSER_CAPABILITIES;
538
- }
539
- isSecureContext() {
540
- return typeof window !== 'undefined' && window.isSecureContext;
541
- }
542
- isSupported(capability) {
543
- switch (capability) {
544
- case 'permissions':
545
- return typeof navigator !== 'undefined' && 'permissions' in navigator;
546
- case 'geolocation':
547
- return typeof navigator !== 'undefined' && 'geolocation' in navigator;
548
- case 'clipboard':
549
- return typeof navigator !== 'undefined' && 'clipboard' in navigator;
550
- case 'notification':
551
- return typeof window !== 'undefined' && 'Notification' in window;
552
- case 'mediaDevices':
553
- case 'camera':
554
- return typeof navigator !== 'undefined' && 'mediaDevices' in navigator;
555
- case 'webWorker':
556
- case 'regexSecurity':
557
- return typeof Worker !== 'undefined';
558
- case 'webStorage':
559
- return typeof Storage !== 'undefined';
560
- case 'webShare':
561
- return typeof navigator !== 'undefined' && 'share' in navigator;
562
- case 'battery':
563
- return typeof navigator !== 'undefined' && 'getBattery' in navigator;
564
- case 'webSocket':
565
- return typeof WebSocket !== 'undefined';
566
- case 'intersectionObserver':
567
- return typeof IntersectionObserver !== 'undefined';
568
- case 'resizeObserver':
569
- return typeof ResizeObserver !== 'undefined';
570
- case 'pageVisibility':
571
- return typeof document !== 'undefined' && 'hidden' in document;
572
- case 'broadcastChannel':
573
- return typeof BroadcastChannel !== 'undefined';
574
- case 'networkInformation':
575
- return (typeof navigator !== 'undefined' &&
576
- ('connection' in navigator || 'mozConnection' in navigator));
577
- case 'screenWakeLock':
578
- return typeof navigator !== 'undefined' && 'wakeLock' in navigator;
579
- case 'screenOrientation':
580
- return typeof screen !== 'undefined' && 'orientation' in screen;
581
- case 'fullscreen':
582
- return (typeof document !== 'undefined' &&
583
- ('fullscreenEnabled' in document || 'webkitFullscreenEnabled' in document));
584
- case 'fileSystemAccess':
585
- return typeof window !== 'undefined' && 'showOpenFilePicker' in window;
586
- case 'mediaRecorder':
587
- return typeof MediaRecorder !== 'undefined';
588
- case 'serverSentEvents':
589
- return typeof EventSource !== 'undefined';
590
- case 'vibration':
591
- return typeof navigator !== 'undefined' && 'vibrate' in navigator;
592
- case 'speechSynthesis':
593
- return typeof window !== 'undefined' && 'speechSynthesis' in window;
594
- case 'mutationObserver':
595
- return typeof MutationObserver !== 'undefined';
596
- case 'performanceObserver':
597
- return typeof PerformanceObserver !== 'undefined';
598
- case 'idleDetector':
599
- return typeof window !== 'undefined' && 'IdleDetector' in window;
600
- case 'eyeDropper':
601
- return typeof window !== 'undefined' && 'EyeDropper' in window;
602
- case 'barcodeDetector':
603
- return typeof window !== 'undefined' && 'BarcodeDetector' in window;
604
- case 'webAudio':
605
- return typeof window !== 'undefined' && 'AudioContext' in window;
606
- case 'gamepad':
607
- return typeof navigator !== 'undefined' && 'getGamepads' in navigator;
608
- case 'webBluetooth':
609
- return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
610
- case 'webUsb':
611
- return typeof navigator !== 'undefined' && 'usb' in navigator;
612
- case 'webNfc':
613
- return typeof window !== 'undefined' && 'NDEFReader' in window;
614
- case 'paymentRequest':
615
- return typeof window !== 'undefined' && 'PaymentRequest' in window;
616
- case 'credentialManagement':
617
- return typeof navigator !== 'undefined' && 'credentials' in navigator;
618
- default:
619
- return false;
620
- }
621
- }
622
- getAllStatuses() {
623
- const secureContext = this.isSecureContext();
624
- return this.getCapabilities().map((capability) => ({
625
- id: capability.id,
626
- label: capability.label,
627
- supported: this.isSupported(capability.id),
628
- secureContext,
629
- requiresSecureContext: capability.requiresSecureContext,
630
- }));
631
- }
632
- async getPermissionState(permission) {
633
- if (typeof navigator === 'undefined' || !('permissions' in navigator)) {
634
- return 'unknown';
635
- }
636
- try {
637
- const status = await navigator.permissions.query({ name: permission });
638
- return status.state;
639
- }
640
- catch {
641
- return 'unknown';
642
- }
643
- }
644
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
645
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, providedIn: 'root' });
646
- }
647
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, decorators: [{
648
- type: Injectable,
649
- args: [{ providedIn: 'root' }]
650
- }] });
651
-
652
672
  class BatteryService extends BrowserApiBaseService {
653
673
  batteryManager = null;
654
674
  getApiName() {
655
675
  return 'battery';
656
676
  }
657
- ensureSupported() {
658
- super.ensureSupported();
659
- const nav = navigator;
660
- if (!('getBattery' in nav)) {
661
- throw new Error('Battery Status API not supported in this browser');
662
- }
677
+ getCapabilityId() {
678
+ return 'battery';
663
679
  }
664
680
  async initialize() {
665
681
  this.ensureSupported();
@@ -739,11 +755,8 @@ class WebShareService extends BrowserApiBaseService {
739
755
  getApiName() {
740
756
  return 'web-share';
741
757
  }
742
- ensureSupported() {
743
- super.ensureSupported();
744
- if (!('share' in navigator)) {
745
- throw new Error('Web Share API not supported in this browser');
746
- }
758
+ getCapabilityId() {
759
+ return 'webShare';
747
760
  }
748
761
  async share(data) {
749
762
  this.ensureSupported();
@@ -779,255 +792,337 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
779
792
  type: Injectable
780
793
  }] });
781
794
 
782
- class WebStorageService extends BrowserApiBaseService {
783
- storageEvents = signal(null, ...(ngDevMode ? [{ debugName: "storageEvents" }] : /* istanbul ignore next */ []));
784
- constructor() {
785
- super();
786
- this.setupEventListeners();
787
- }
788
- getApiName() {
789
- return 'storage';
790
- }
791
- ensureSupported() {
792
- super.ensureSupported();
793
- if (typeof Storage === 'undefined') {
794
- throw new Error('Storage API not supported in this browser');
795
- }
796
- }
797
- setupEventListeners() {
798
- if (this.isBrowserEnvironment()) {
799
- fromEvent(window, 'storage')
800
- .pipe(takeUntilDestroyed(this.destroyRef))
801
- .subscribe((event) => {
802
- const storageEvent = event;
803
- const area = storageEvent.storageArea === localStorage ? 'localStorage' : 'sessionStorage';
804
- this.storageEvents.set({
805
- key: storageEvent.key,
806
- newValue: storageEvent.newValue ? this.deserializeValue(storageEvent.newValue) : null,
807
- oldValue: storageEvent.oldValue ? this.deserializeValue(storageEvent.oldValue) : null,
808
- storageArea: area,
809
- });
810
- });
811
- }
812
- }
813
- serializeValue(value, options) {
814
- if (options?.serialize) {
815
- return options.serialize(value);
816
- }
817
- return JSON.stringify(value);
795
+ const SECURITY_WARN_KEY = Symbol('storage-security-warned');
796
+ /**
797
+ * Implementation of `StorageNamespace` shared by `local` and `session` namespaces.
798
+ * Wraps every native access in try/catch so Safari private mode and sandboxed iframes
799
+ * (which throw `SecurityError`) degrade gracefully instead of crashing the app.
800
+ */
801
+ class StorageNamespaceImpl {
802
+ area;
803
+ events;
804
+ logger;
805
+ supportedCache = null;
806
+ constructor(area, events, logger) {
807
+ this.area = area;
808
+ this.events = events;
809
+ this.logger = logger;
818
810
  }
819
- deserializeValue(value, options) {
820
- if (value === null)
821
- return null;
822
- if (options?.deserialize) {
823
- return options.deserialize(value);
811
+ isSupported() {
812
+ if (this.supportedCache !== null)
813
+ return this.supportedCache;
814
+ if (typeof window === 'undefined' || typeof Storage === 'undefined') {
815
+ this.supportedCache = false;
816
+ return false;
824
817
  }
825
818
  try {
826
- return JSON.parse(value);
819
+ const store = this.getStore();
820
+ const probe = `__bwa_probe_${Date.now()}__`;
821
+ store.setItem(probe, '1');
822
+ store.removeItem(probe);
823
+ this.supportedCache = true;
827
824
  }
828
825
  catch {
829
- return value;
826
+ this.warnSecurityOnce();
827
+ this.supportedCache = false;
830
828
  }
829
+ return this.supportedCache;
831
830
  }
832
- getKey(key, options) {
833
- const prefix = options?.prefix || '';
834
- return prefix ? `${prefix}:${key}` : key;
835
- }
836
- emitStorageChange(fullKey, newValue, oldValue, area) {
837
- this.storageEvents.set({ key: fullKey, newValue, oldValue, storageArea: area });
838
- }
839
- // Local Storage Methods
840
- setLocalStorage(key, value, options = {}) {
841
- this.ensureSupported();
831
+ set(key, value, opts = {}) {
832
+ if (!this.isSupported())
833
+ return false;
842
834
  try {
843
- const serializedValue = this.serializeValue(value, options);
844
- const fullKey = this.getKey(key, options);
845
- const oldRaw = localStorage.getItem(fullKey);
846
- const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
847
- localStorage.setItem(fullKey, serializedValue);
848
- this.emitStorageChange(fullKey, value, oldValue, 'localStorage');
835
+ const fullKey = this.getKey(key, opts);
836
+ const oldRaw = this.getStore().getItem(fullKey);
837
+ const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, opts) : null;
838
+ const serialized = this.serializeValue(value, opts);
839
+ this.getStore().setItem(fullKey, serialized);
840
+ this.events.emit({ key: fullKey, newValue: value, oldValue, storageArea: this.area });
849
841
  return true;
850
842
  }
851
843
  catch (error) {
852
- this.logError('Error setting localStorage:', error);
844
+ this.logger.error(`[storage:${this.area}] set("${key}") failed`, error);
853
845
  return false;
854
846
  }
855
847
  }
856
- getLocalStorage(key, defaultValue = null, options = {}) {
857
- this.ensureSupported();
848
+ get(key, defaultValue = null, opts = {}) {
849
+ if (!this.isSupported())
850
+ return defaultValue;
858
851
  try {
859
- const fullKey = this.getKey(key, options);
860
- const value = localStorage.getItem(fullKey);
861
- return value !== null ? this.deserializeValue(value, options) : defaultValue;
852
+ const fullKey = this.getKey(key, opts);
853
+ const raw = this.getStore().getItem(fullKey);
854
+ return raw !== null ? this.deserializeValue(raw, opts) : defaultValue;
862
855
  }
863
856
  catch (error) {
864
- this.logError('Error getting localStorage:', error);
857
+ this.logger.error(`[storage:${this.area}] get("${key}") failed`, error);
865
858
  return defaultValue;
866
859
  }
867
860
  }
868
- removeLocalStorage(key, options = {}) {
869
- this.ensureSupported();
861
+ remove(key, opts = {}) {
862
+ if (!this.isSupported())
863
+ return false;
870
864
  try {
871
- const fullKey = this.getKey(key, options);
872
- const oldRaw = localStorage.getItem(fullKey);
873
- const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
874
- localStorage.removeItem(fullKey);
875
- this.emitStorageChange(fullKey, null, oldValue, 'localStorage');
865
+ const fullKey = this.getKey(key, opts);
866
+ const oldRaw = this.getStore().getItem(fullKey);
867
+ const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, opts) : null;
868
+ this.getStore().removeItem(fullKey);
869
+ this.events.emit({ key: fullKey, newValue: null, oldValue, storageArea: this.area });
876
870
  return true;
877
871
  }
878
872
  catch (error) {
879
- this.logError('Error removing localStorage:', error);
873
+ this.logger.error(`[storage:${this.area}] remove("${key}") failed`, error);
880
874
  return false;
881
875
  }
882
876
  }
883
- clearLocalStorage(options = {}) {
884
- this.ensureSupported();
877
+ clear(opts = {}) {
878
+ if (!this.isSupported())
879
+ return false;
885
880
  try {
886
- const prefix = options?.prefix;
881
+ const prefix = opts?.prefix;
882
+ const store = this.getStore();
887
883
  if (prefix) {
888
- const keysToRemove = [];
889
- for (let i = 0; i < localStorage.length; i++) {
890
- const key = localStorage.key(i);
891
- if (key && key.startsWith(`${prefix}:`)) {
892
- keysToRemove.push(key);
893
- }
884
+ const toRemove = [];
885
+ for (let i = 0; i < store.length; i++) {
886
+ const k = store.key(i);
887
+ if (k && k.startsWith(`${prefix}:`))
888
+ toRemove.push(k);
889
+ }
890
+ for (const k of toRemove) {
891
+ const oldRaw = store.getItem(k);
892
+ store.removeItem(k);
893
+ this.events.emit({
894
+ key: k,
895
+ newValue: null,
896
+ oldValue: oldRaw,
897
+ storageArea: this.area,
898
+ });
894
899
  }
895
- keysToRemove.forEach((key) => {
896
- const oldRaw = localStorage.getItem(key);
897
- localStorage.removeItem(key);
898
- this.emitStorageChange(key, null, oldRaw, 'localStorage');
899
- });
900
900
  }
901
901
  else {
902
- localStorage.clear();
903
- this.emitStorageChange(null, null, null, 'localStorage');
902
+ store.clear();
903
+ this.events.emit({ key: null, newValue: null, oldValue: null, storageArea: this.area });
904
904
  }
905
905
  return true;
906
906
  }
907
907
  catch (error) {
908
- this.logError('Error clearing localStorage:', error);
908
+ this.logger.error(`[storage:${this.area}] clear() failed`, error);
909
909
  return false;
910
910
  }
911
911
  }
912
- // Session Storage Methods
913
- setSessionStorage(key, value, options = {}) {
914
- this.ensureSupported();
912
+ size(opts = {}) {
913
+ if (!this.isSupported())
914
+ return 0;
915
915
  try {
916
- const serializedValue = this.serializeValue(value, options);
917
- const fullKey = this.getKey(key, options);
918
- const oldRaw = sessionStorage.getItem(fullKey);
919
- const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
920
- sessionStorage.setItem(fullKey, serializedValue);
921
- this.emitStorageChange(fullKey, value, oldValue, 'sessionStorage');
922
- return true;
916
+ const store = this.getStore();
917
+ const prefix = opts?.prefix;
918
+ let total = 0;
919
+ for (let i = 0; i < store.length; i++) {
920
+ const k = store.key(i);
921
+ if (k && (!prefix || k.startsWith(`${prefix}:`))) {
922
+ total += (store.getItem(k)?.length ?? 0) + k.length;
923
+ }
924
+ }
925
+ return total;
923
926
  }
924
927
  catch (error) {
925
- this.logError('Error setting sessionStorage:', error);
926
- return false;
928
+ this.logger.error(`[storage:${this.area}] size() failed`, error);
929
+ return 0;
927
930
  }
928
931
  }
929
- getSessionStorage(key, defaultValue = null, options = {}) {
930
- this.ensureSupported();
932
+ watch(key, opts = {}) {
933
+ const fullKey = this.getKey(key, opts);
934
+ return this.events.events$.pipe(filter((event) => event.storageArea === this.area && (event.key === null || event.key === fullKey)), map((event) => event.newValue));
935
+ }
936
+ native() {
937
+ if (!this.isSupported()) {
938
+ throw new Error(`${this.area} not supported in this environment`);
939
+ }
940
+ return this.getStore();
941
+ }
942
+ getStore() {
943
+ return this.area === 'localStorage' ? window.localStorage : window.sessionStorage;
944
+ }
945
+ getKey(key, opts) {
946
+ const prefix = opts?.prefix ?? '';
947
+ return prefix ? `${prefix}:${key}` : key;
948
+ }
949
+ serializeValue(value, opts) {
950
+ if (opts?.serialize)
951
+ return opts.serialize(value);
952
+ return JSON.stringify(value);
953
+ }
954
+ deserializeValue(value, opts) {
955
+ if (opts?.deserialize)
956
+ return opts.deserialize(value);
931
957
  try {
932
- const fullKey = this.getKey(key, options);
933
- const value = sessionStorage.getItem(fullKey);
934
- return value !== null ? this.deserializeValue(value, options) : defaultValue;
958
+ return JSON.parse(value);
935
959
  }
936
- catch (error) {
937
- this.logError('Error getting sessionStorage:', error);
938
- return defaultValue;
960
+ catch {
961
+ return value;
939
962
  }
940
963
  }
941
- removeSessionStorage(key, options = {}) {
942
- this.ensureSupported();
964
+ warnSecurityOnce() {
965
+ const holder = globalThis;
966
+ if (!holder[SECURITY_WARN_KEY])
967
+ holder[SECURITY_WARN_KEY] = new Set();
968
+ if (holder[SECURITY_WARN_KEY].has(this.area))
969
+ return;
970
+ holder[SECURITY_WARN_KEY].add(this.area);
971
+ this.logger.warn(`[storage:${this.area}] access denied (SecurityError). Common causes: Safari private mode, ` +
972
+ 'sandboxed iframes, or browser storage disabled. Falling back to default values.');
973
+ }
974
+ }
975
+
976
+ let legacyDeprecationLogged$1 = false;
977
+ /**
978
+ * Web Storage service with two namespaces (`local`, `session`) sharing one method
979
+ * surface. SecurityError-safe (Safari private mode, sandboxed iframes return defaults
980
+ * instead of throwing).
981
+ *
982
+ * Preferred usage:
983
+ * ```ts
984
+ * const storage = inject(WebStorageService);
985
+ * storage.local.set('user', { id: 1 });
986
+ * const user = storage.local.get<{ id: number }>('user');
987
+ * storage.local.watch<{ id: number }>('user').subscribe(console.log);
988
+ * ```
989
+ *
990
+ * Legacy methods (`setLocalStorage`, `getLocalStorage`, etc.) remain as deprecated
991
+ * wrappers for one minor cycle; removal slated for v22.
992
+ */
993
+ class WebStorageService extends BrowserApiBaseService {
994
+ storageLogger = inject(BROWSER_API_LOGGER);
995
+ storageEvents = signal(null, ...(ngDevMode ? [{ debugName: "storageEvents" }] : /* istanbul ignore next */ []));
996
+ eventBus = {
997
+ emit: (event) => this.storageEvents.set(event),
998
+ events$: toObservable(this.storageEvents).pipe(filter((event) => event !== null), distinctUntilChanged((a, b) => a.key === b.key &&
999
+ a.newValue === b.newValue &&
1000
+ a.oldValue === b.oldValue &&
1001
+ a.storageArea === b.storageArea)),
1002
+ };
1003
+ /** Local storage namespace. */
1004
+ local = new StorageNamespaceImpl('localStorage', this.eventBus, this.storageLogger);
1005
+ /** Session storage namespace. */
1006
+ session = new StorageNamespaceImpl('sessionStorage', this.eventBus, this.storageLogger);
1007
+ constructor() {
1008
+ super();
1009
+ this.setupCrossTabListener();
1010
+ }
1011
+ getApiName() {
1012
+ return 'storage';
1013
+ }
1014
+ getCapabilityId() {
1015
+ return 'webStorage';
1016
+ }
1017
+ /** Returns true if either local or session storage is usable. */
1018
+ isSupported() {
1019
+ return this.local.isSupported() || this.session.isSupported();
1020
+ }
1021
+ /** Stream of every storage mutation observed in this tab or other tabs. */
1022
+ getStorageEvents() {
1023
+ return this.eventBus.events$;
1024
+ }
1025
+ setupCrossTabListener() {
1026
+ if (!this.isBrowserEnvironment())
1027
+ return;
1028
+ fromEvent(window, 'storage')
1029
+ .pipe(takeUntilDestroyed(this.destroyRef))
1030
+ .subscribe((event) => {
1031
+ const area = event.storageArea === window.localStorage ? 'localStorage' : 'sessionStorage';
1032
+ this.storageEvents.set({
1033
+ key: event.key,
1034
+ newValue: event.newValue ? this.safeParse(event.newValue) : null,
1035
+ oldValue: event.oldValue ? this.safeParse(event.oldValue) : null,
1036
+ storageArea: area,
1037
+ });
1038
+ });
1039
+ }
1040
+ safeParse(value) {
943
1041
  try {
944
- const fullKey = this.getKey(key, options);
945
- const oldRaw = sessionStorage.getItem(fullKey);
946
- const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
947
- sessionStorage.removeItem(fullKey);
948
- this.emitStorageChange(fullKey, null, oldValue, 'sessionStorage');
949
- return true;
1042
+ return JSON.parse(value);
950
1043
  }
951
- catch (error) {
952
- this.logError('Error removing sessionStorage:', error);
953
- return false;
1044
+ catch {
1045
+ return value;
954
1046
  }
955
1047
  }
1048
+ // ---------- legacy API (deprecated) ----------
1049
+ /** @deprecated Use `storage.local.set(key, value, opts)`. Removed in v22. */
1050
+ setLocalStorage(key, value, options = {}) {
1051
+ this.warnLegacyOnce();
1052
+ return this.local.set(key, value, options);
1053
+ }
1054
+ /** @deprecated Use `storage.local.get(key, defaultValue, opts)`. Removed in v22. */
1055
+ getLocalStorage(key, defaultValue = null, options = {}) {
1056
+ this.warnLegacyOnce();
1057
+ return this.local.get(key, defaultValue, options);
1058
+ }
1059
+ /** @deprecated Use `storage.local.remove(key, opts)`. Removed in v22. */
1060
+ removeLocalStorage(key, options = {}) {
1061
+ this.warnLegacyOnce();
1062
+ return this.local.remove(key, options);
1063
+ }
1064
+ /** @deprecated Use `storage.local.clear(opts)`. Removed in v22. */
1065
+ clearLocalStorage(options = {}) {
1066
+ this.warnLegacyOnce();
1067
+ return this.local.clear(options);
1068
+ }
1069
+ /** @deprecated Use `storage.session.set(key, value, opts)`. Removed in v22. */
1070
+ setSessionStorage(key, value, options = {}) {
1071
+ this.warnLegacyOnce();
1072
+ return this.session.set(key, value, options);
1073
+ }
1074
+ /** @deprecated Use `storage.session.get(key, defaultValue, opts)`. Removed in v22. */
1075
+ getSessionStorage(key, defaultValue = null, options = {}) {
1076
+ this.warnLegacyOnce();
1077
+ return this.session.get(key, defaultValue, options);
1078
+ }
1079
+ /** @deprecated Use `storage.session.remove(key, opts)`. Removed in v22. */
1080
+ removeSessionStorage(key, options = {}) {
1081
+ this.warnLegacyOnce();
1082
+ return this.session.remove(key, options);
1083
+ }
1084
+ /** @deprecated Use `storage.session.clear(opts)`. Removed in v22. */
956
1085
  clearSessionStorage(options = {}) {
957
- this.ensureSupported();
958
- try {
959
- const prefix = options?.prefix;
960
- if (prefix) {
961
- const keysToRemove = [];
962
- for (let i = 0; i < sessionStorage.length; i++) {
963
- const key = sessionStorage.key(i);
964
- if (key && key.startsWith(`${prefix}:`)) {
965
- keysToRemove.push(key);
966
- }
967
- }
968
- keysToRemove.forEach((key) => {
969
- const oldRaw = sessionStorage.getItem(key);
970
- sessionStorage.removeItem(key);
971
- this.emitStorageChange(key, null, oldRaw, 'sessionStorage');
972
- });
973
- }
974
- else {
975
- sessionStorage.clear();
976
- this.emitStorageChange(null, null, null, 'sessionStorage');
977
- }
978
- return true;
979
- }
980
- catch (error) {
981
- this.logError('Error clearing sessionStorage:', error);
982
- return false;
983
- }
1086
+ this.warnLegacyOnce();
1087
+ return this.session.clear(options);
984
1088
  }
985
- // Utility Methods
1089
+ /** @deprecated Use `storage.local.size(opts)`. Removed in v22. */
986
1090
  getLocalStorageSize(options = {}) {
987
- this.ensureSupported();
988
- let totalSize = 0;
989
- const prefix = options?.prefix;
990
- for (let i = 0; i < localStorage.length; i++) {
991
- const key = localStorage.key(i);
992
- if (key && (!prefix || key.startsWith(`${prefix}:`))) {
993
- totalSize += (localStorage.getItem(key)?.length || 0) + key.length;
994
- }
995
- }
996
- return totalSize;
1091
+ this.warnLegacyOnce();
1092
+ return this.local.size(options);
997
1093
  }
1094
+ /** @deprecated Use `storage.session.size(opts)`. Removed in v22. */
998
1095
  getSessionStorageSize(options = {}) {
999
- this.ensureSupported();
1000
- let totalSize = 0;
1001
- const prefix = options?.prefix;
1002
- for (let i = 0; i < sessionStorage.length; i++) {
1003
- const key = sessionStorage.key(i);
1004
- if (key && (!prefix || key.startsWith(`${prefix}:`))) {
1005
- totalSize += (sessionStorage.getItem(key)?.length || 0) + key.length;
1006
- }
1007
- }
1008
- return totalSize;
1009
- }
1010
- getStorageEvents() {
1011
- return toObservable(this.storageEvents).pipe(filter((event) => event !== null), distinctUntilChanged((prev, curr) => prev.key === curr.key &&
1012
- prev.newValue === curr.newValue &&
1013
- prev.oldValue === curr.oldValue));
1096
+ this.warnLegacyOnce();
1097
+ return this.session.size(options);
1014
1098
  }
1099
+ /** @deprecated Use `storage.local.watch<T>(key, opts)`. Removed in v22. */
1015
1100
  watchLocalStorage(key, options = {}) {
1016
- const fullKey = this.getKey(key, options);
1017
- return this.getStorageEvents().pipe(filter((event) => event.storageArea === 'localStorage' && (event.key === null || event.key === fullKey)), map((event) => event.newValue));
1101
+ this.warnLegacyOnce();
1102
+ return this.local.watch(key, options);
1018
1103
  }
1104
+ /** @deprecated Use `storage.session.watch<T>(key, opts)`. Removed in v22. */
1019
1105
  watchSessionStorage(key, options = {}) {
1020
- const fullKey = this.getKey(key, options);
1021
- return this.getStorageEvents().pipe(filter((event) => event.storageArea === 'sessionStorage' && (event.key === null || event.key === fullKey)), map((event) => event.newValue));
1106
+ this.warnLegacyOnce();
1107
+ return this.session.watch(key, options);
1022
1108
  }
1023
- // Direct access to native storage APIs
1109
+ /** @deprecated Use `storage.local.native()`. Removed in v22. */
1024
1110
  getNativeLocalStorage() {
1025
- this.ensureSupported();
1026
- return localStorage;
1111
+ this.warnLegacyOnce();
1112
+ return this.local.native();
1027
1113
  }
1114
+ /** @deprecated Use `storage.session.native()`. Removed in v22. */
1028
1115
  getNativeSessionStorage() {
1029
- this.ensureSupported();
1030
- return sessionStorage;
1116
+ this.warnLegacyOnce();
1117
+ return this.session.native();
1118
+ }
1119
+ warnLegacyOnce() {
1120
+ if (legacyDeprecationLogged$1)
1121
+ return;
1122
+ legacyDeprecationLogged$1 = true;
1123
+ this.storageLogger.warn('[storage] WebStorageService.{set,get,remove,clear,watch}{Local,Session}Storage are ' +
1124
+ 'deprecated. Use storage.local and storage.session namespaces. Legacy methods will be ' +
1125
+ 'removed in v22.');
1031
1126
  }
1032
1127
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1033
1128
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService });
@@ -1037,7 +1132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
1037
1132
  }], ctorParameters: () => [] });
1038
1133
 
1039
1134
  const DEFAULT_MAX_RECONNECT_DELAY = 30_000;
1040
- const DEFAULT_REQUEST_TIMEOUT = 30_000;
1135
+ const DEFAULT_REQUEST_TIMEOUT$1 = 30_000;
1041
1136
  /**
1042
1137
  * Stateful WebSocket client wrapping a single connection. One instance per logical
1043
1138
  * connection (do NOT share between `connect()` calls).
@@ -1109,7 +1204,7 @@ class WebSocketClient {
1109
1204
  */
1110
1205
  request(type, data, opts) {
1111
1206
  const id = this.generateId();
1112
- const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
1207
+ const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT$1;
1113
1208
  return new Promise((resolve, reject) => {
1114
1209
  const timer = setTimeout(() => {
1115
1210
  this.pendingRequests.delete(id);
@@ -1321,11 +1416,8 @@ class WebSocketService extends BrowserApiBaseService {
1321
1416
  getApiName() {
1322
1417
  return 'websocket';
1323
1418
  }
1324
- ensureSupported() {
1325
- super.ensureSupported();
1326
- if (typeof WebSocket === 'undefined') {
1327
- throw new Error('WebSocket API not supported in this browser');
1328
- }
1419
+ getCapabilityId() {
1420
+ return 'webSocket';
1329
1421
  }
1330
1422
  /**
1331
1423
  * Create a new WebSocket client. The client owns one connection and is the recommended
@@ -1483,162 +1575,259 @@ function toObservableLike(client) {
1483
1575
  });
1484
1576
  }
1485
1577
 
1578
+ const DEFAULT_REQUEST_TIMEOUT = 30_000;
1579
+ /**
1580
+ * Service for creating and managing Web Workers with first-class support for
1581
+ * request/response over `postMessage` (id correlation, timeout, transferables).
1582
+ *
1583
+ * Status is exposed both as a `Signal<WorkerStatus>` (preferred via
1584
+ * {@link getStatusSignal}) and as an `Observable<WorkerStatus>` for backward
1585
+ * compatibility ({@link createWorker}, {@link getStatus}).
1586
+ *
1587
+ * The service registers a single `DestroyRef.onDestroy` in its constructor that
1588
+ * terminates every registered worker; per-worker handlers do not register their
1589
+ * own cleanup callbacks (avoids accumulating callbacks per `setupWorker` call,
1590
+ * a previous leak source).
1591
+ */
1486
1592
  class WebWorkerService extends BrowserApiBaseService {
1487
- workers = new Map();
1488
- workerStatuses = new Map();
1489
- workerMessages = new Map();
1490
- currentWorkerStatuses = new Map();
1593
+ workerLogger = inject(BROWSER_API_LOGGER);
1594
+ entries = new Map();
1491
1595
  _cleanup = this.destroyRef.onDestroy(() => this.terminateAllWorkers());
1492
1596
  getApiName() {
1493
1597
  return 'webworker';
1494
1598
  }
1495
- ensureSupported() {
1496
- super.ensureSupported();
1497
- if (typeof Worker === 'undefined') {
1498
- throw new Error('Web Workers not supported in this browser');
1499
- }
1599
+ getCapabilityId() {
1600
+ return 'webWorker';
1500
1601
  }
1602
+ /**
1603
+ * Create a worker. Idempotent: calling twice with the same name returns the
1604
+ * existing entry without recreating the worker.
1605
+ *
1606
+ * Returns an `Observable<WorkerStatus>` for backward compatibility. Prefer
1607
+ * {@link createWorkerSignal} for new code.
1608
+ */
1501
1609
  createWorker(name, scriptUrl) {
1610
+ const status = this.createWorkerSignal(name, scriptUrl);
1611
+ return toObservable(status);
1612
+ }
1613
+ /**
1614
+ * Create a worker (signal-first). Returns the status signal; status is also
1615
+ * accessible later via {@link getStatusSignal}.
1616
+ */
1617
+ createWorkerSignal(name, scriptUrl) {
1502
1618
  this.ensureSupported();
1503
- return new Observable((observer) => {
1504
- if (this.workers.has(name)) {
1505
- observer.next(this.currentWorkerStatuses.get(name));
1506
- return () => {
1507
- // No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
1508
- };
1509
- }
1510
- try {
1511
- const worker = new Worker(scriptUrl);
1512
- this.workers.set(name, worker);
1513
- this.setupWorker(name, worker);
1514
- const status = {
1515
- initialized: true,
1516
- running: true,
1517
- messageCount: 0,
1518
- };
1519
- this.currentWorkerStatuses.set(name, status);
1520
- this.updateWorkerStatus(name, status);
1521
- observer.next(status);
1522
- }
1523
- catch (error) {
1524
- this.logError(`Failed to create worker ${name}:`, error);
1525
- const status = {
1526
- initialized: false,
1527
- running: false,
1528
- error: error instanceof Error ? error.message : 'Unknown error',
1529
- messageCount: 0,
1530
- };
1531
- this.currentWorkerStatuses.set(name, status);
1532
- this.updateWorkerStatus(name, status);
1533
- observer.next(status);
1534
- }
1535
- return () => {
1536
- // No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
1537
- };
1538
- });
1619
+ const existing = this.entries.get(name);
1620
+ if (existing && existing.worker)
1621
+ return existing.status.asReadonly();
1622
+ const entry = this.ensureEntry(name);
1623
+ try {
1624
+ const worker = new Worker(scriptUrl);
1625
+ entry.worker = worker;
1626
+ this.attachHandlers(name, entry);
1627
+ entry.status.set({ initialized: true, running: true, messageCount: 0 });
1628
+ }
1629
+ catch (error) {
1630
+ const message = error instanceof Error ? error.message : 'Unknown error';
1631
+ entry.status.set({
1632
+ initialized: false,
1633
+ running: false,
1634
+ error: message,
1635
+ messageCount: 0,
1636
+ });
1637
+ this.workerLogger.error(`[webworker] Failed to create worker "${name}"`, error);
1638
+ }
1639
+ return entry.status.asReadonly();
1539
1640
  }
1540
1641
  terminateWorker(name) {
1541
- const worker = this.workers.get(name);
1542
- if (worker) {
1543
- worker.terminate();
1544
- this.workers.delete(name);
1545
- this.workerStatuses.delete(name);
1546
- this.workerMessages.delete(name);
1547
- this.currentWorkerStatuses.delete(name);
1548
- }
1642
+ const entry = this.entries.get(name);
1643
+ if (!entry)
1644
+ return;
1645
+ this.rejectPending(entry, new Error(`Worker "${name}" terminated`));
1646
+ if (entry.worker)
1647
+ entry.worker.terminate();
1648
+ entry.messages$.complete();
1649
+ this.entries.delete(name);
1549
1650
  }
1550
1651
  terminateAllWorkers() {
1551
- this.workers.forEach((_, name) => {
1652
+ for (const name of [...this.entries.keys()]) {
1552
1653
  this.terminateWorker(name);
1553
- });
1654
+ }
1554
1655
  }
1656
+ /** Send a fire-and-forget message. Use {@link request} when you need a reply. */
1555
1657
  postMessage(workerName, task) {
1556
- const worker = this.workers.get(workerName);
1557
- if (!worker) {
1558
- this.logError(`Worker ${workerName} not found`);
1658
+ const entry = this.entries.get(workerName);
1659
+ if (!entry || !entry.worker) {
1660
+ this.workerLogger.error(`[webworker] postMessage: worker "${workerName}" not found`);
1559
1661
  return;
1560
1662
  }
1663
+ const message = {
1664
+ id: task.id ?? this.generateId(),
1665
+ type: task.type,
1666
+ data: task.data,
1667
+ timestamp: Date.now(),
1668
+ };
1561
1669
  try {
1562
- const message = { ...task, timestamp: Date.now() };
1563
1670
  if (task.transferable) {
1564
- worker.postMessage(message, task.transferable);
1671
+ entry.worker.postMessage(message, task.transferable);
1565
1672
  }
1566
1673
  else {
1567
- worker.postMessage(message);
1568
- }
1569
- const currentStatus = this.currentWorkerStatuses.get(workerName);
1570
- if (currentStatus) {
1571
- currentStatus.messageCount++;
1572
- this.updateWorkerStatus(workerName, currentStatus);
1674
+ entry.worker.postMessage(message);
1573
1675
  }
1676
+ this.bumpMessageCount(entry);
1574
1677
  }
1575
1678
  catch (error) {
1576
- this.logError(`Failed to post message to worker ${workerName}:`, error);
1679
+ this.workerLogger.error(`[webworker] postMessage failed for "${workerName}"`, error);
1577
1680
  }
1578
1681
  }
1579
- getMessages(workerName) {
1580
- if (!this.workerMessages.has(workerName)) {
1581
- this.workerMessages.set(workerName, new Subject());
1682
+ /**
1683
+ * Send a message and await a correlated response. The worker MUST send back a
1684
+ * message containing `correlationId` matching the request id.
1685
+ *
1686
+ * ```ts
1687
+ * const result = await ws.request<{ ok: boolean }>('worker', 'compute', { n: 1 });
1688
+ * ```
1689
+ */
1690
+ request(workerName, type, data, opts) {
1691
+ const entry = this.entries.get(workerName);
1692
+ if (!entry || !entry.worker) {
1693
+ return Promise.reject(new Error(`Worker "${workerName}" not found`));
1582
1694
  }
1583
- return this.workerMessages.get(workerName).asObservable();
1695
+ const id = this.generateId();
1696
+ const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
1697
+ return new Promise((resolve, reject) => {
1698
+ const timer = setTimeout(() => {
1699
+ entry.pending.delete(id);
1700
+ reject(new Error(`WebWorker "${workerName}" request timeout after ${timeoutMs}ms`));
1701
+ }, timeoutMs);
1702
+ entry.pending.set(id, {
1703
+ resolve: resolve,
1704
+ reject,
1705
+ timer,
1706
+ });
1707
+ const message = {
1708
+ id,
1709
+ type,
1710
+ data,
1711
+ timestamp: Date.now(),
1712
+ correlationId: id,
1713
+ };
1714
+ try {
1715
+ if (opts?.transferable) {
1716
+ entry.worker.postMessage(message, opts.transferable);
1717
+ }
1718
+ else {
1719
+ entry.worker.postMessage(message);
1720
+ }
1721
+ this.bumpMessageCount(entry);
1722
+ }
1723
+ catch (error) {
1724
+ clearTimeout(timer);
1725
+ entry.pending.delete(id);
1726
+ reject(error instanceof Error ? error : new Error('postMessage failed'));
1727
+ }
1728
+ });
1729
+ }
1730
+ getMessages(workerName) {
1731
+ const entry = this.ensureEntry(workerName);
1732
+ return entry.messages$.asObservable();
1733
+ }
1734
+ getMessagesByType(workerName, type) {
1735
+ return this.getMessages(workerName).pipe(filter((m) => m.type === type));
1584
1736
  }
1737
+ /** @deprecated Use {@link getStatusSignal}. Kept as Observable for backward compat. */
1585
1738
  getStatus(workerName) {
1586
- if (!this.workerStatuses.has(workerName)) {
1587
- this.workerStatuses.set(workerName, new Subject());
1588
- }
1589
- return this.workerStatuses.get(workerName).asObservable();
1739
+ return toObservable(this.getStatusSignal(workerName));
1740
+ }
1741
+ getStatusSignal(workerName) {
1742
+ return this.ensureEntry(workerName).status.asReadonly();
1590
1743
  }
1591
1744
  getCurrentStatus(workerName) {
1592
- return this.currentWorkerStatuses.get(workerName);
1745
+ return this.entries.get(workerName)?.status();
1593
1746
  }
1594
1747
  getAllStatuses() {
1595
- return new Map(this.currentWorkerStatuses);
1748
+ const result = new Map();
1749
+ for (const [name, entry] of this.entries)
1750
+ result.set(name, entry.status());
1751
+ return result;
1596
1752
  }
1597
1753
  isWorkerRunning(workerName) {
1598
- const status = this.currentWorkerStatuses.get(workerName);
1599
- return status?.running ?? false;
1754
+ return this.entries.get(workerName)?.status().running ?? false;
1755
+ }
1756
+ getNativeWorker(name) {
1757
+ return this.entries.get(name)?.worker ?? undefined;
1758
+ }
1759
+ getAllWorkers() {
1760
+ const result = new Map();
1761
+ for (const [name, entry] of this.entries) {
1762
+ if (entry.worker)
1763
+ result.set(name, entry.worker);
1764
+ }
1765
+ return result;
1600
1766
  }
1601
- setupWorker(name, worker) {
1602
- worker.onmessage = (event) => {
1767
+ // ---------- internals ----------
1768
+ attachHandlers(name, entry) {
1769
+ if (!entry.worker)
1770
+ return;
1771
+ entry.worker.onmessage = (event) => {
1772
+ const data = event.data ?? {};
1773
+ const correlationId = data.correlationId ?? data.id;
1774
+ if (correlationId && entry.pending.has(correlationId)) {
1775
+ const p = entry.pending.get(correlationId);
1776
+ clearTimeout(p.timer);
1777
+ entry.pending.delete(correlationId);
1778
+ p.resolve(data.data);
1779
+ return;
1780
+ }
1603
1781
  const message = {
1604
- id: event.data.id || `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
1605
- type: event.data.type || 'message',
1606
- data: event.data.data,
1607
- timestamp: event.data.timestamp || Date.now(),
1782
+ id: data.id ?? this.generateId(),
1783
+ type: data.type ?? 'message',
1784
+ data: data.data,
1785
+ timestamp: data.timestamp ?? Date.now(),
1786
+ correlationId: data.correlationId,
1608
1787
  };
1609
- if (!this.workerMessages.has(name)) {
1610
- this.workerMessages.set(name, new Subject());
1611
- }
1612
- this.workerMessages.get(name).next(message);
1788
+ entry.messages$.next(message);
1613
1789
  };
1614
- worker.onerror = (error) => {
1615
- this.logError(`Worker ${name} error:`, error);
1616
- const status = {
1617
- initialized: true,
1790
+ entry.worker.onerror = (error) => {
1791
+ this.workerLogger.error(`[webworker] Worker "${name}" error`, error);
1792
+ entry.status.update((s) => ({
1793
+ ...s,
1618
1794
  running: false,
1619
1795
  error: error instanceof Error ? error.message : 'Worker error',
1620
- messageCount: this.currentWorkerStatuses.get(name)?.messageCount ?? 0,
1621
- };
1622
- this.currentWorkerStatuses.set(name, status);
1623
- this.updateWorkerStatus(name, status);
1796
+ }));
1624
1797
  };
1625
- // Auto-cleanup when service is destroyed
1626
- this.destroyRef.onDestroy(() => {
1627
- this.terminateWorker(name);
1628
- });
1629
1798
  }
1630
- updateWorkerStatus(name, status) {
1631
- if (!this.workerStatuses.has(name)) {
1632
- this.workerStatuses.set(name, new Subject());
1799
+ rejectPending(entry, reason) {
1800
+ for (const p of entry.pending.values()) {
1801
+ clearTimeout(p.timer);
1802
+ p.reject(reason);
1633
1803
  }
1634
- this.workerStatuses.get(name).next(status);
1804
+ entry.pending.clear();
1635
1805
  }
1636
- // Direct access to native Worker API
1637
- getNativeWorker(name) {
1638
- return this.workers.get(name);
1806
+ bumpMessageCount(entry) {
1807
+ entry.status.update((s) => ({ ...s, messageCount: s.messageCount + 1 }));
1639
1808
  }
1640
- getAllWorkers() {
1641
- return new Map(this.workers);
1809
+ ensureEntry(workerName) {
1810
+ let entry = this.entries.get(workerName);
1811
+ if (!entry) {
1812
+ entry = {
1813
+ worker: null,
1814
+ status: signal({
1815
+ initialized: false,
1816
+ running: false,
1817
+ messageCount: 0,
1818
+ }),
1819
+ messages$: new Subject(),
1820
+ pending: new Map(),
1821
+ };
1822
+ this.entries.set(workerName, entry);
1823
+ }
1824
+ return entry;
1825
+ }
1826
+ generateId() {
1827
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
1828
+ return globalThis.crypto.randomUUID();
1829
+ }
1830
+ return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
1642
1831
  }
1643
1832
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1644
1833
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService });
@@ -1678,8 +1867,8 @@ class IntersectionObserverService extends BrowserApiBaseService {
1678
1867
  getApiName() {
1679
1868
  return 'intersection-observer';
1680
1869
  }
1681
- isSupported() {
1682
- return this.isBrowserEnvironment() && 'IntersectionObserver' in window;
1870
+ getCapabilityId() {
1871
+ return 'intersectionObserver';
1683
1872
  }
1684
1873
  observe(element, options = {}) {
1685
1874
  if (!this.isSupported()) {
@@ -1738,8 +1927,8 @@ class ResizeObserverService extends BrowserApiBaseService {
1738
1927
  getApiName() {
1739
1928
  return 'resize-observer';
1740
1929
  }
1741
- isSupported() {
1742
- return this.isBrowserEnvironment() && 'ResizeObserver' in window;
1930
+ getCapabilityId() {
1931
+ return 'resizeObserver';
1743
1932
  }
1744
1933
  observe(element, options = {}) {
1745
1934
  if (!this.isSupported()) {
@@ -1779,8 +1968,8 @@ class PageVisibilityService extends BrowserApiBaseService {
1779
1968
  getApiName() {
1780
1969
  return 'page-visibility';
1781
1970
  }
1782
- isSupported() {
1783
- return this.isBrowserEnvironment() && 'hidden' in document;
1971
+ getCapabilityId() {
1972
+ return 'pageVisibility';
1784
1973
  }
1785
1974
  get isHidden() {
1786
1975
  if (!this.isSupported())
@@ -1856,13 +2045,11 @@ class BroadcastChannelService extends ConnectionRegistryBaseService {
1856
2045
  closeNativeConnection(channel) {
1857
2046
  channel.close();
1858
2047
  }
1859
- isSupported() {
1860
- return this.isBrowserEnvironment() && 'BroadcastChannel' in window;
2048
+ getCapabilityId() {
2049
+ return 'broadcastChannel';
1861
2050
  }
1862
2051
  ensureBroadcastChannelSupported() {
1863
- if (!this.isSupported()) {
1864
- throw new Error('BroadcastChannel API not supported in this environment');
1865
- }
2052
+ this.ensureSupported();
1866
2053
  }
1867
2054
  open(name) {
1868
2055
  this.ensureBroadcastChannelSupported();
@@ -1959,8 +2146,8 @@ class NetworkInformationService extends BrowserApiBaseService {
1959
2146
  getApiName() {
1960
2147
  return 'network-information';
1961
2148
  }
1962
- isSupported() {
1963
- return this.isBrowserEnvironment() && isNetworkInformationSupported();
2149
+ getCapabilityId() {
2150
+ return 'networkInformation';
1964
2151
  }
1965
2152
  getSnapshot() {
1966
2153
  return this.isBrowserEnvironment() ? getNetworkSnapshot() : { online: true };
@@ -1983,8 +2170,8 @@ class ScreenWakeLockService extends BrowserApiBaseService {
1983
2170
  getApiName() {
1984
2171
  return 'screen-wake-lock';
1985
2172
  }
1986
- isSupported() {
1987
- return this.isBrowserEnvironment() && 'wakeLock' in navigator;
2173
+ getCapabilityId() {
2174
+ return 'screenWakeLock';
1988
2175
  }
1989
2176
  get isActive() {
1990
2177
  return this.sentinel !== null && !this.sentinel.released;
@@ -2078,8 +2265,8 @@ class ScreenOrientationService extends BrowserApiBaseService {
2078
2265
  getApiName() {
2079
2266
  return 'screen-orientation';
2080
2267
  }
2081
- isSupported() {
2082
- return this.isBrowserEnvironment() && 'screen' in window && 'orientation' in screen;
2268
+ getCapabilityId() {
2269
+ return 'screenOrientation';
2083
2270
  }
2084
2271
  getSnapshot() {
2085
2272
  return this.isBrowserEnvironment()
@@ -2123,8 +2310,12 @@ class FullscreenService extends BrowserApiBaseService {
2123
2310
  getApiName() {
2124
2311
  return 'fullscreen';
2125
2312
  }
2313
+ getCapabilityId() {
2314
+ return 'fullscreen';
2315
+ }
2316
+ /** Override to also check the *enabled* flag (browser may have disabled fullscreen). */
2126
2317
  isSupported() {
2127
- if (!this.isBrowserEnvironment())
2318
+ if (!super.isSupported())
2128
2319
  return false;
2129
2320
  return !!(document.fullscreenEnabled ??
2130
2321
  document.webkitFullscreenEnabled);
@@ -2215,17 +2406,18 @@ class FileSystemAccessService extends BrowserApiBaseService {
2215
2406
  getApiName() {
2216
2407
  return 'file-system-access';
2217
2408
  }
2409
+ getCapabilityId() {
2410
+ return 'fileSystemAccess';
2411
+ }
2412
+ /** Override to also assert secure context (required by the spec). */
2218
2413
  isSupported() {
2219
- return this.isBrowserEnvironment() && 'showOpenFilePicker' in window && window.isSecureContext;
2414
+ return super.isSupported() && typeof window !== 'undefined' && window.isSecureContext;
2220
2415
  }
2221
2416
  get win() {
2222
2417
  return window;
2223
2418
  }
2224
2419
  ensureSupported() {
2225
2420
  super.ensureSupported();
2226
- if (!('showOpenFilePicker' in window)) {
2227
- throw new Error('File System Access API not supported in this browser');
2228
- }
2229
2421
  if (!window.isSecureContext) {
2230
2422
  throw new Error('File System Access API requires a secure context (HTTPS)');
2231
2423
  }
@@ -2301,8 +2493,8 @@ class MediaRecorderService extends BrowserApiBaseService {
2301
2493
  getApiName() {
2302
2494
  return 'media-recorder';
2303
2495
  }
2304
- isSupported() {
2305
- return this.isBrowserEnvironment() && 'MediaRecorder' in window;
2496
+ getCapabilityId() {
2497
+ return 'mediaRecorder';
2306
2498
  }
2307
2499
  get state() {
2308
2500
  return this.recorder?.state ?? 'inactive';
@@ -2392,13 +2584,11 @@ class ServerSentEventsService extends ConnectionRegistryBaseService {
2392
2584
  closeNativeConnection(source) {
2393
2585
  source.close();
2394
2586
  }
2395
- isSupported() {
2396
- return this.isBrowserEnvironment() && 'EventSource' in window;
2587
+ getCapabilityId() {
2588
+ return 'serverSentEvents';
2397
2589
  }
2398
2590
  ensureSSESupported() {
2399
- if (!this.isSupported()) {
2400
- throw new Error('Server-Sent Events (EventSource) not supported in this environment');
2401
- }
2591
+ this.ensureSupported();
2402
2592
  }
2403
2593
  connect(url, config = {}) {
2404
2594
  this.ensureSSESupported();
@@ -2478,8 +2668,8 @@ class VibrationService extends BrowserApiBaseService {
2478
2668
  notification: [200],
2479
2669
  doubleTap: [50, 100, 50],
2480
2670
  };
2481
- isSupported() {
2482
- return this.isBrowserEnvironment() && 'vibrate' in navigator;
2671
+ getCapabilityId() {
2672
+ return 'vibration';
2483
2673
  }
2484
2674
  vibrate(pattern = 200) {
2485
2675
  if (!this.isSupported())
@@ -2512,13 +2702,11 @@ class SpeechSynthesisService extends BrowserApiBaseService {
2512
2702
  getApiName() {
2513
2703
  return 'speech-synthesis';
2514
2704
  }
2515
- isSupported() {
2516
- return this.isBrowserEnvironment() && 'speechSynthesis' in window;
2705
+ getCapabilityId() {
2706
+ return 'speechSynthesis';
2517
2707
  }
2518
2708
  ensureSpeechSynthesisSupported() {
2519
- if (!this.isSupported()) {
2520
- throw new Error('Speech Synthesis API not supported in this browser');
2521
- }
2709
+ this.ensureSupported();
2522
2710
  }
2523
2711
  get state() {
2524
2712
  if (!this.isSupported())
@@ -2628,8 +2816,8 @@ class MutationObserverService extends BrowserApiBaseService {
2628
2816
  getApiName() {
2629
2817
  return 'mutation-observer';
2630
2818
  }
2631
- isSupported() {
2632
- return this.isBrowserEnvironment() && isMutationObserverSupported();
2819
+ getCapabilityId() {
2820
+ return 'mutationObserver';
2633
2821
  }
2634
2822
  observe(target, options) {
2635
2823
  if (!this.isSupported()) {
@@ -2666,8 +2854,8 @@ class PerformanceObserverService extends BrowserApiBaseService {
2666
2854
  getApiName() {
2667
2855
  return 'performance-observer';
2668
2856
  }
2669
- isSupported() {
2670
- return this.isBrowserEnvironment() && isPerformanceObserverSupported();
2857
+ getCapabilityId() {
2858
+ return 'performanceObserver';
2671
2859
  }
2672
2860
  observe(config) {
2673
2861
  if (!this.isSupported()) {
@@ -2695,8 +2883,8 @@ class WebAudioService extends BrowserApiBaseService {
2695
2883
  return 'web-audio';
2696
2884
  }
2697
2885
  context = null;
2698
- isSupported() {
2699
- return this.isBrowserEnvironment() && 'AudioContext' in window;
2886
+ getCapabilityId() {
2887
+ return 'webAudio';
2700
2888
  }
2701
2889
  getContext() {
2702
2890
  if (!this.isSupported()) {
@@ -2857,8 +3045,8 @@ class GamepadService extends BrowserApiBaseService {
2857
3045
  getApiName() {
2858
3046
  return 'gamepad';
2859
3047
  }
2860
- isSupported() {
2861
- return this.isBrowserEnvironment() && isGamepadSupported();
3048
+ getCapabilityId() {
3049
+ return 'gamepad';
2862
3050
  }
2863
3051
  getSnapshot(index) {
2864
3052
  if (!this.isSupported())
@@ -3078,6 +3266,257 @@ function injectGamepad(index, intervalMs = 16) {
3078
3266
  };
3079
3267
  }
3080
3268
 
3269
+ function injectClipboard() {
3270
+ const destroyRef = inject(DestroyRef);
3271
+ const platformId = inject(PLATFORM_ID);
3272
+ const isBrowser = isPlatformBrowser(platformId);
3273
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && !!navigator.clipboard, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3274
+ const text = signal(null, ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
3275
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3276
+ const busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
3277
+ let disposed = false;
3278
+ destroyRef.onDestroy(() => {
3279
+ disposed = true;
3280
+ });
3281
+ const writeText = async (value) => {
3282
+ if (!supported() || disposed) {
3283
+ error.set('Clipboard API not supported');
3284
+ return false;
3285
+ }
3286
+ busy.set(true);
3287
+ try {
3288
+ await navigator.clipboard.writeText(value);
3289
+ if (!disposed) {
3290
+ text.set(value);
3291
+ error.set(null);
3292
+ }
3293
+ return true;
3294
+ }
3295
+ catch (err) {
3296
+ if (!disposed)
3297
+ error.set(err instanceof Error ? err.message : 'writeText failed');
3298
+ return false;
3299
+ }
3300
+ finally {
3301
+ if (!disposed)
3302
+ busy.set(false);
3303
+ }
3304
+ };
3305
+ const readText = async () => {
3306
+ if (!supported() || disposed) {
3307
+ error.set('Clipboard API not supported');
3308
+ return null;
3309
+ }
3310
+ busy.set(true);
3311
+ try {
3312
+ const value = await navigator.clipboard.readText();
3313
+ if (!disposed) {
3314
+ text.set(value);
3315
+ error.set(null);
3316
+ }
3317
+ return value;
3318
+ }
3319
+ catch (err) {
3320
+ if (!disposed)
3321
+ error.set(err instanceof Error ? err.message : 'readText failed');
3322
+ return null;
3323
+ }
3324
+ finally {
3325
+ if (!disposed)
3326
+ busy.set(false);
3327
+ }
3328
+ };
3329
+ return {
3330
+ text: text.asReadonly(),
3331
+ error: error.asReadonly(),
3332
+ busy: busy.asReadonly(),
3333
+ isSupported: supported.asReadonly(),
3334
+ writeText,
3335
+ readText,
3336
+ };
3337
+ }
3338
+
3339
+ function injectGeolocation(opts = {}) {
3340
+ const destroyRef = inject(DestroyRef);
3341
+ const platformId = inject(PLATFORM_ID);
3342
+ const isBrowser = isPlatformBrowser(platformId);
3343
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'geolocation' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3344
+ const position = signal(null, ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
3345
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3346
+ const watching = signal(false, ...(ngDevMode ? [{ debugName: "watching" }] : /* istanbul ignore next */ []));
3347
+ let watchId = null;
3348
+ const watch = (positionOpts) => {
3349
+ if (!supported() || watchId !== null)
3350
+ return;
3351
+ watching.set(true);
3352
+ watchId = navigator.geolocation.watchPosition((pos) => {
3353
+ position.set(pos);
3354
+ error.set(null);
3355
+ }, (err) => error.set(err), positionOpts);
3356
+ };
3357
+ const stop = () => {
3358
+ if (watchId !== null) {
3359
+ navigator.geolocation.clearWatch(watchId);
3360
+ watchId = null;
3361
+ }
3362
+ watching.set(false);
3363
+ };
3364
+ const getCurrent = (positionOpts) => {
3365
+ if (!supported()) {
3366
+ return Promise.reject(new Error('Geolocation API not supported'));
3367
+ }
3368
+ return new Promise((resolve, reject) => {
3369
+ navigator.geolocation.getCurrentPosition((pos) => {
3370
+ position.set(pos);
3371
+ error.set(null);
3372
+ resolve(pos);
3373
+ }, (err) => {
3374
+ error.set(err);
3375
+ reject(err);
3376
+ }, positionOpts);
3377
+ });
3378
+ };
3379
+ destroyRef.onDestroy(() => stop());
3380
+ if (opts.watch)
3381
+ watch(opts);
3382
+ return {
3383
+ position: position.asReadonly(),
3384
+ error: error.asReadonly(),
3385
+ watching: watching.asReadonly(),
3386
+ isSupported: supported.asReadonly(),
3387
+ watch,
3388
+ stop,
3389
+ getCurrent,
3390
+ };
3391
+ }
3392
+
3393
+ function injectBattery() {
3394
+ const destroyRef = inject(DestroyRef);
3395
+ const platformId = inject(PLATFORM_ID);
3396
+ const isBrowser = isPlatformBrowser(platformId);
3397
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'getBattery' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3398
+ const info = signal(null, ...(ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []));
3399
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3400
+ let manager = null;
3401
+ let disposed = false;
3402
+ const snapshot = () => manager
3403
+ ? {
3404
+ charging: manager.charging,
3405
+ level: manager.level,
3406
+ chargingTime: manager.chargingTime,
3407
+ dischargingTime: manager.dischargingTime,
3408
+ }
3409
+ : null;
3410
+ const update = () => {
3411
+ if (!disposed)
3412
+ info.set(snapshot());
3413
+ };
3414
+ const events = [
3415
+ 'chargingchange',
3416
+ 'levelchange',
3417
+ 'chargingtimechange',
3418
+ 'dischargingtimechange',
3419
+ ];
3420
+ const refresh = async () => {
3421
+ if (!supported() || disposed)
3422
+ return;
3423
+ try {
3424
+ if (!manager) {
3425
+ const nav = navigator;
3426
+ manager = await nav.getBattery();
3427
+ for (const ev of events) {
3428
+ manager.addEventListener(ev, update);
3429
+ }
3430
+ }
3431
+ update();
3432
+ }
3433
+ catch (err) {
3434
+ if (!disposed)
3435
+ error.set(err instanceof Error ? err.message : 'getBattery failed');
3436
+ }
3437
+ };
3438
+ destroyRef.onDestroy(() => {
3439
+ disposed = true;
3440
+ if (manager) {
3441
+ for (const ev of events) {
3442
+ manager.removeEventListener(ev, update);
3443
+ }
3444
+ manager = null;
3445
+ }
3446
+ });
3447
+ if (supported())
3448
+ void refresh();
3449
+ return {
3450
+ info: info.asReadonly(),
3451
+ error: error.asReadonly(),
3452
+ isSupported: supported.asReadonly(),
3453
+ refresh,
3454
+ };
3455
+ }
3456
+
3457
+ function injectWakeLock() {
3458
+ const destroyRef = inject(DestroyRef);
3459
+ const platformId = inject(PLATFORM_ID);
3460
+ const isBrowser = isPlatformBrowser(platformId);
3461
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'wakeLock' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3462
+ const active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
3463
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3464
+ let sentinel = null;
3465
+ let disposed = false;
3466
+ const onRelease = () => {
3467
+ if (!disposed)
3468
+ active.set(false);
3469
+ sentinel = null;
3470
+ };
3471
+ const request = async () => {
3472
+ if (!supported() || disposed) {
3473
+ error.set('Screen Wake Lock API not supported');
3474
+ return false;
3475
+ }
3476
+ if (sentinel && !sentinel.released)
3477
+ return true;
3478
+ try {
3479
+ const nav = navigator;
3480
+ sentinel = await nav.wakeLock.request('screen');
3481
+ sentinel.addEventListener('release', onRelease);
3482
+ if (!disposed) {
3483
+ active.set(true);
3484
+ error.set(null);
3485
+ }
3486
+ return true;
3487
+ }
3488
+ catch (err) {
3489
+ if (!disposed)
3490
+ error.set(err instanceof Error ? err.message : 'wakeLock.request failed');
3491
+ return false;
3492
+ }
3493
+ };
3494
+ const release = async () => {
3495
+ if (!sentinel || sentinel.released)
3496
+ return;
3497
+ try {
3498
+ await sentinel.release();
3499
+ }
3500
+ finally {
3501
+ onRelease();
3502
+ }
3503
+ };
3504
+ destroyRef.onDestroy(() => {
3505
+ disposed = true;
3506
+ if (sentinel && !sentinel.released) {
3507
+ void sentinel.release();
3508
+ }
3509
+ sentinel = null;
3510
+ });
3511
+ return {
3512
+ active: active.asReadonly(),
3513
+ error: error.asReadonly(),
3514
+ isSupported: supported.asReadonly(),
3515
+ request,
3516
+ release,
3517
+ };
3518
+ }
3519
+
3081
3520
  class BrowserSupportUtil {
3082
3521
  static isSupported(feature) {
3083
3522
  if (typeof window === 'undefined' || typeof navigator === 'undefined') {
@@ -3399,4 +3838,4 @@ const version = '0.1.0';
3399
3838
  * Generated bundle index. Do not edit.
3400
3839
  */
3401
3840
 
3402
- export { BROWSER_API_LOGGER, BatteryService, BroadcastChannelService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, ConnectionRegistryBaseService, FileSystemAccessService, FullscreenService, GamepadService, GeolocationService, IntersectionObserverService, MediaDevicesService, MediaRecorderService, MutationObserverService, NetworkInformationService, NotificationService, PageVisibilityService, PerformanceObserverService, PermissionsService, ResizeObserverService, ScreenOrientationService, ScreenWakeLockService, ServerSentEventsService, SpeechSynthesisService, VibrationService, WebAudioService, WebShareService, WebSocketClient, WebSocketService, WebStorageService, WebWorkerService, permissionGuard as createPermissionGuard, defaultBrowserWebApisConfig, injectGamepad, injectIntersectionObserver, injectMutationObserver, injectNetworkInformation, injectPageVisibility, injectPerformanceObserver, injectResizeObserver, injectScreenOrientation, permissionGuard, provideBattery, provideBroadcastChannel, provideBrowserWebApis, provideCamera, provideClipboard, provideCommunicationApis, provideFileSystemAccess, provideFullscreen, provideGamepad, provideGeolocation, provideIntersectionObserver, provideLocationApis, provideMediaApis, provideMediaDevices, provideMediaRecorder, provideMutationObserver, provideNetworkInformation, provideNotifications, providePageVisibility, providePerformanceObserver, providePermissions, provideResizeObserver, provideScreenOrientation, provideScreenWakeLock, provideServerSentEvents, provideSpeechSynthesis, provideStorageApis, provideVibration, provideWebAudio, provideWebShare, provideWebSocket, provideWebStorage, provideWebWorker, version };
3841
+ export { BROWSER_API_LOGGER, BatteryService, BroadcastChannelService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, ConnectionRegistryBaseService, FileSystemAccessService, FullscreenService, GamepadService, GeolocationService, IntersectionObserverService, MediaDevicesService, MediaRecorderService, MutationObserverService, NetworkInformationService, NotificationService, PageVisibilityService, PerformanceObserverService, PermissionsService, ResizeObserverService, ScreenOrientationService, ScreenWakeLockService, ServerSentEventsService, SpeechSynthesisService, VibrationService, WebAudioService, WebShareService, WebSocketClient, WebSocketService, WebStorageService, WebWorkerService, permissionGuard as createPermissionGuard, defaultBrowserWebApisConfig, injectBattery, injectClipboard, injectGamepad, injectGeolocation, injectIntersectionObserver, injectMutationObserver, injectNetworkInformation, injectPageVisibility, injectPerformanceObserver, injectResizeObserver, injectScreenOrientation, injectWakeLock, permissionGuard, provideBattery, provideBroadcastChannel, provideBrowserWebApis, provideCamera, provideClipboard, provideCommunicationApis, provideFileSystemAccess, provideFullscreen, provideGamepad, provideGeolocation, provideIntersectionObserver, provideLocationApis, provideMediaApis, provideMediaDevices, provideMediaRecorder, provideMutationObserver, provideNetworkInformation, provideNotifications, providePageVisibility, providePerformanceObserver, providePermissions, provideResizeObserver, provideScreenOrientation, provideScreenWakeLock, provideServerSentEvents, provideSpeechSynthesis, provideStorageApis, provideVibration, provideWebAudio, provideWebShare, provideWebSocket, provideWebStorage, provideWebWorker, version };