@angular-helpers/browser-web-apis 21.7.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,5 +1,5 @@
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
5
  import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -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,193 +633,40 @@ 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) {
466
642
  throw new Error('Clipboard API not supported \u2014 a secure context (HTTPS) is required');
467
643
  }
468
- }
469
- async writeText(text) {
470
- this.ensureSupported();
471
- try {
472
- await navigator.clipboard.writeText(text);
473
- }
474
- catch (error) {
475
- this.logError('Error writing to clipboard:', error);
476
- throw error;
477
- }
478
- }
479
- async readText() {
480
- this.ensureSupported();
481
- try {
482
- return await navigator.clipboard.readText();
483
- }
484
- catch (error) {
485
- this.logError('Error reading from clipboard:', error);
486
- throw error;
487
- }
488
- }
489
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
490
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService });
491
- }
492
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, decorators: [{
493
- type: Injectable
494
- }] });
495
-
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';
644
+ }
645
+ async writeText(text) {
646
+ this.ensureSupported();
647
+ try {
648
+ await navigator.clipboard.writeText(text);
649
+ }
650
+ catch (error) {
651
+ this.logError('Error writing to clipboard:', error);
652
+ throw error;
635
653
  }
654
+ }
655
+ async readText() {
656
+ this.ensureSupported();
636
657
  try {
637
- const status = await navigator.permissions.query({ name: permission });
638
- return status.state;
658
+ return await navigator.clipboard.readText();
639
659
  }
640
- catch {
641
- return 'unknown';
660
+ catch (error) {
661
+ this.logError('Error reading from clipboard:', error);
662
+ throw error;
642
663
  }
643
664
  }
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' });
665
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
666
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService });
646
667
  }
647
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, decorators: [{
648
- type: Injectable,
649
- args: [{ providedIn: 'root' }]
668
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, decorators: [{
669
+ type: Injectable
650
670
  }] });
651
671
 
652
672
  class BatteryService extends BrowserApiBaseService {
@@ -654,12 +674,8 @@ class BatteryService extends BrowserApiBaseService {
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();
@@ -998,11 +1011,8 @@ class WebStorageService extends BrowserApiBaseService {
998
1011
  getApiName() {
999
1012
  return 'storage';
1000
1013
  }
1001
- ensureSupported() {
1002
- super.ensureSupported();
1003
- if (typeof Storage === 'undefined') {
1004
- throw new Error('Storage API not supported in this browser');
1005
- }
1014
+ getCapabilityId() {
1015
+ return 'webStorage';
1006
1016
  }
1007
1017
  /** Returns true if either local or session storage is usable. */
1008
1018
  isSupported() {
@@ -1122,7 +1132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
1122
1132
  }], ctorParameters: () => [] });
1123
1133
 
1124
1134
  const DEFAULT_MAX_RECONNECT_DELAY = 30_000;
1125
- const DEFAULT_REQUEST_TIMEOUT = 30_000;
1135
+ const DEFAULT_REQUEST_TIMEOUT$1 = 30_000;
1126
1136
  /**
1127
1137
  * Stateful WebSocket client wrapping a single connection. One instance per logical
1128
1138
  * connection (do NOT share between `connect()` calls).
@@ -1194,7 +1204,7 @@ class WebSocketClient {
1194
1204
  */
1195
1205
  request(type, data, opts) {
1196
1206
  const id = this.generateId();
1197
- const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
1207
+ const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT$1;
1198
1208
  return new Promise((resolve, reject) => {
1199
1209
  const timer = setTimeout(() => {
1200
1210
  this.pendingRequests.delete(id);
@@ -1406,11 +1416,8 @@ class WebSocketService extends BrowserApiBaseService {
1406
1416
  getApiName() {
1407
1417
  return 'websocket';
1408
1418
  }
1409
- ensureSupported() {
1410
- super.ensureSupported();
1411
- if (typeof WebSocket === 'undefined') {
1412
- throw new Error('WebSocket API not supported in this browser');
1413
- }
1419
+ getCapabilityId() {
1420
+ return 'webSocket';
1414
1421
  }
1415
1422
  /**
1416
1423
  * Create a new WebSocket client. The client owns one connection and is the recommended
@@ -1568,162 +1575,259 @@ function toObservableLike(client) {
1568
1575
  });
1569
1576
  }
1570
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
+ */
1571
1592
  class WebWorkerService extends BrowserApiBaseService {
1572
- workers = new Map();
1573
- workerStatuses = new Map();
1574
- workerMessages = new Map();
1575
- currentWorkerStatuses = new Map();
1593
+ workerLogger = inject(BROWSER_API_LOGGER);
1594
+ entries = new Map();
1576
1595
  _cleanup = this.destroyRef.onDestroy(() => this.terminateAllWorkers());
1577
1596
  getApiName() {
1578
1597
  return 'webworker';
1579
1598
  }
1580
- ensureSupported() {
1581
- super.ensureSupported();
1582
- if (typeof Worker === 'undefined') {
1583
- throw new Error('Web Workers not supported in this browser');
1584
- }
1599
+ getCapabilityId() {
1600
+ return 'webWorker';
1585
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
+ */
1586
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) {
1587
1618
  this.ensureSupported();
1588
- return new Observable((observer) => {
1589
- if (this.workers.has(name)) {
1590
- observer.next(this.currentWorkerStatuses.get(name));
1591
- return () => {
1592
- // No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
1593
- };
1594
- }
1595
- try {
1596
- const worker = new Worker(scriptUrl);
1597
- this.workers.set(name, worker);
1598
- this.setupWorker(name, worker);
1599
- const status = {
1600
- initialized: true,
1601
- running: true,
1602
- messageCount: 0,
1603
- };
1604
- this.currentWorkerStatuses.set(name, status);
1605
- this.updateWorkerStatus(name, status);
1606
- observer.next(status);
1607
- }
1608
- catch (error) {
1609
- this.logError(`Failed to create worker ${name}:`, error);
1610
- const status = {
1611
- initialized: false,
1612
- running: false,
1613
- error: error instanceof Error ? error.message : 'Unknown error',
1614
- messageCount: 0,
1615
- };
1616
- this.currentWorkerStatuses.set(name, status);
1617
- this.updateWorkerStatus(name, status);
1618
- observer.next(status);
1619
- }
1620
- return () => {
1621
- // No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
1622
- };
1623
- });
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();
1624
1640
  }
1625
1641
  terminateWorker(name) {
1626
- const worker = this.workers.get(name);
1627
- if (worker) {
1628
- worker.terminate();
1629
- this.workers.delete(name);
1630
- this.workerStatuses.delete(name);
1631
- this.workerMessages.delete(name);
1632
- this.currentWorkerStatuses.delete(name);
1633
- }
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);
1634
1650
  }
1635
1651
  terminateAllWorkers() {
1636
- this.workers.forEach((_, name) => {
1652
+ for (const name of [...this.entries.keys()]) {
1637
1653
  this.terminateWorker(name);
1638
- });
1654
+ }
1639
1655
  }
1656
+ /** Send a fire-and-forget message. Use {@link request} when you need a reply. */
1640
1657
  postMessage(workerName, task) {
1641
- const worker = this.workers.get(workerName);
1642
- if (!worker) {
1643
- 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`);
1644
1661
  return;
1645
1662
  }
1663
+ const message = {
1664
+ id: task.id ?? this.generateId(),
1665
+ type: task.type,
1666
+ data: task.data,
1667
+ timestamp: Date.now(),
1668
+ };
1646
1669
  try {
1647
- const message = { ...task, timestamp: Date.now() };
1648
1670
  if (task.transferable) {
1649
- worker.postMessage(message, task.transferable);
1671
+ entry.worker.postMessage(message, task.transferable);
1650
1672
  }
1651
1673
  else {
1652
- worker.postMessage(message);
1653
- }
1654
- const currentStatus = this.currentWorkerStatuses.get(workerName);
1655
- if (currentStatus) {
1656
- currentStatus.messageCount++;
1657
- this.updateWorkerStatus(workerName, currentStatus);
1674
+ entry.worker.postMessage(message);
1658
1675
  }
1676
+ this.bumpMessageCount(entry);
1659
1677
  }
1660
1678
  catch (error) {
1661
- this.logError(`Failed to post message to worker ${workerName}:`, error);
1679
+ this.workerLogger.error(`[webworker] postMessage failed for "${workerName}"`, error);
1662
1680
  }
1663
1681
  }
1664
- getMessages(workerName) {
1665
- if (!this.workerMessages.has(workerName)) {
1666
- 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`));
1667
1694
  }
1668
- 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));
1669
1736
  }
1737
+ /** @deprecated Use {@link getStatusSignal}. Kept as Observable for backward compat. */
1670
1738
  getStatus(workerName) {
1671
- if (!this.workerStatuses.has(workerName)) {
1672
- this.workerStatuses.set(workerName, new Subject());
1673
- }
1674
- return this.workerStatuses.get(workerName).asObservable();
1739
+ return toObservable(this.getStatusSignal(workerName));
1740
+ }
1741
+ getStatusSignal(workerName) {
1742
+ return this.ensureEntry(workerName).status.asReadonly();
1675
1743
  }
1676
1744
  getCurrentStatus(workerName) {
1677
- return this.currentWorkerStatuses.get(workerName);
1745
+ return this.entries.get(workerName)?.status();
1678
1746
  }
1679
1747
  getAllStatuses() {
1680
- 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;
1681
1752
  }
1682
1753
  isWorkerRunning(workerName) {
1683
- const status = this.currentWorkerStatuses.get(workerName);
1684
- 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;
1685
1766
  }
1686
- setupWorker(name, worker) {
1687
- 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
+ }
1688
1781
  const message = {
1689
- id: event.data.id || `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
1690
- type: event.data.type || 'message',
1691
- data: event.data.data,
1692
- 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,
1693
1787
  };
1694
- if (!this.workerMessages.has(name)) {
1695
- this.workerMessages.set(name, new Subject());
1696
- }
1697
- this.workerMessages.get(name).next(message);
1788
+ entry.messages$.next(message);
1698
1789
  };
1699
- worker.onerror = (error) => {
1700
- this.logError(`Worker ${name} error:`, error);
1701
- const status = {
1702
- initialized: true,
1790
+ entry.worker.onerror = (error) => {
1791
+ this.workerLogger.error(`[webworker] Worker "${name}" error`, error);
1792
+ entry.status.update((s) => ({
1793
+ ...s,
1703
1794
  running: false,
1704
1795
  error: error instanceof Error ? error.message : 'Worker error',
1705
- messageCount: this.currentWorkerStatuses.get(name)?.messageCount ?? 0,
1706
- };
1707
- this.currentWorkerStatuses.set(name, status);
1708
- this.updateWorkerStatus(name, status);
1796
+ }));
1709
1797
  };
1710
- // Auto-cleanup when service is destroyed
1711
- this.destroyRef.onDestroy(() => {
1712
- this.terminateWorker(name);
1713
- });
1714
1798
  }
1715
- updateWorkerStatus(name, status) {
1716
- if (!this.workerStatuses.has(name)) {
1717
- 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);
1718
1803
  }
1719
- this.workerStatuses.get(name).next(status);
1804
+ entry.pending.clear();
1720
1805
  }
1721
- // Direct access to native Worker API
1722
- getNativeWorker(name) {
1723
- return this.workers.get(name);
1806
+ bumpMessageCount(entry) {
1807
+ entry.status.update((s) => ({ ...s, messageCount: s.messageCount + 1 }));
1724
1808
  }
1725
- getAllWorkers() {
1726
- 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)}`;
1727
1831
  }
1728
1832
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1729
1833
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService });
@@ -1763,8 +1867,8 @@ class IntersectionObserverService extends BrowserApiBaseService {
1763
1867
  getApiName() {
1764
1868
  return 'intersection-observer';
1765
1869
  }
1766
- isSupported() {
1767
- return this.isBrowserEnvironment() && 'IntersectionObserver' in window;
1870
+ getCapabilityId() {
1871
+ return 'intersectionObserver';
1768
1872
  }
1769
1873
  observe(element, options = {}) {
1770
1874
  if (!this.isSupported()) {
@@ -1823,8 +1927,8 @@ class ResizeObserverService extends BrowserApiBaseService {
1823
1927
  getApiName() {
1824
1928
  return 'resize-observer';
1825
1929
  }
1826
- isSupported() {
1827
- return this.isBrowserEnvironment() && 'ResizeObserver' in window;
1930
+ getCapabilityId() {
1931
+ return 'resizeObserver';
1828
1932
  }
1829
1933
  observe(element, options = {}) {
1830
1934
  if (!this.isSupported()) {
@@ -1864,8 +1968,8 @@ class PageVisibilityService extends BrowserApiBaseService {
1864
1968
  getApiName() {
1865
1969
  return 'page-visibility';
1866
1970
  }
1867
- isSupported() {
1868
- return this.isBrowserEnvironment() && 'hidden' in document;
1971
+ getCapabilityId() {
1972
+ return 'pageVisibility';
1869
1973
  }
1870
1974
  get isHidden() {
1871
1975
  if (!this.isSupported())
@@ -1941,13 +2045,11 @@ class BroadcastChannelService extends ConnectionRegistryBaseService {
1941
2045
  closeNativeConnection(channel) {
1942
2046
  channel.close();
1943
2047
  }
1944
- isSupported() {
1945
- return this.isBrowserEnvironment() && 'BroadcastChannel' in window;
2048
+ getCapabilityId() {
2049
+ return 'broadcastChannel';
1946
2050
  }
1947
2051
  ensureBroadcastChannelSupported() {
1948
- if (!this.isSupported()) {
1949
- throw new Error('BroadcastChannel API not supported in this environment');
1950
- }
2052
+ this.ensureSupported();
1951
2053
  }
1952
2054
  open(name) {
1953
2055
  this.ensureBroadcastChannelSupported();
@@ -2044,8 +2146,8 @@ class NetworkInformationService extends BrowserApiBaseService {
2044
2146
  getApiName() {
2045
2147
  return 'network-information';
2046
2148
  }
2047
- isSupported() {
2048
- return this.isBrowserEnvironment() && isNetworkInformationSupported();
2149
+ getCapabilityId() {
2150
+ return 'networkInformation';
2049
2151
  }
2050
2152
  getSnapshot() {
2051
2153
  return this.isBrowserEnvironment() ? getNetworkSnapshot() : { online: true };
@@ -2068,8 +2170,8 @@ class ScreenWakeLockService extends BrowserApiBaseService {
2068
2170
  getApiName() {
2069
2171
  return 'screen-wake-lock';
2070
2172
  }
2071
- isSupported() {
2072
- return this.isBrowserEnvironment() && 'wakeLock' in navigator;
2173
+ getCapabilityId() {
2174
+ return 'screenWakeLock';
2073
2175
  }
2074
2176
  get isActive() {
2075
2177
  return this.sentinel !== null && !this.sentinel.released;
@@ -2163,8 +2265,8 @@ class ScreenOrientationService extends BrowserApiBaseService {
2163
2265
  getApiName() {
2164
2266
  return 'screen-orientation';
2165
2267
  }
2166
- isSupported() {
2167
- return this.isBrowserEnvironment() && 'screen' in window && 'orientation' in screen;
2268
+ getCapabilityId() {
2269
+ return 'screenOrientation';
2168
2270
  }
2169
2271
  getSnapshot() {
2170
2272
  return this.isBrowserEnvironment()
@@ -2208,8 +2310,12 @@ class FullscreenService extends BrowserApiBaseService {
2208
2310
  getApiName() {
2209
2311
  return 'fullscreen';
2210
2312
  }
2313
+ getCapabilityId() {
2314
+ return 'fullscreen';
2315
+ }
2316
+ /** Override to also check the *enabled* flag (browser may have disabled fullscreen). */
2211
2317
  isSupported() {
2212
- if (!this.isBrowserEnvironment())
2318
+ if (!super.isSupported())
2213
2319
  return false;
2214
2320
  return !!(document.fullscreenEnabled ??
2215
2321
  document.webkitFullscreenEnabled);
@@ -2300,17 +2406,18 @@ class FileSystemAccessService extends BrowserApiBaseService {
2300
2406
  getApiName() {
2301
2407
  return 'file-system-access';
2302
2408
  }
2409
+ getCapabilityId() {
2410
+ return 'fileSystemAccess';
2411
+ }
2412
+ /** Override to also assert secure context (required by the spec). */
2303
2413
  isSupported() {
2304
- return this.isBrowserEnvironment() && 'showOpenFilePicker' in window && window.isSecureContext;
2414
+ return super.isSupported() && typeof window !== 'undefined' && window.isSecureContext;
2305
2415
  }
2306
2416
  get win() {
2307
2417
  return window;
2308
2418
  }
2309
2419
  ensureSupported() {
2310
2420
  super.ensureSupported();
2311
- if (!('showOpenFilePicker' in window)) {
2312
- throw new Error('File System Access API not supported in this browser');
2313
- }
2314
2421
  if (!window.isSecureContext) {
2315
2422
  throw new Error('File System Access API requires a secure context (HTTPS)');
2316
2423
  }
@@ -2386,8 +2493,8 @@ class MediaRecorderService extends BrowserApiBaseService {
2386
2493
  getApiName() {
2387
2494
  return 'media-recorder';
2388
2495
  }
2389
- isSupported() {
2390
- return this.isBrowserEnvironment() && 'MediaRecorder' in window;
2496
+ getCapabilityId() {
2497
+ return 'mediaRecorder';
2391
2498
  }
2392
2499
  get state() {
2393
2500
  return this.recorder?.state ?? 'inactive';
@@ -2477,13 +2584,11 @@ class ServerSentEventsService extends ConnectionRegistryBaseService {
2477
2584
  closeNativeConnection(source) {
2478
2585
  source.close();
2479
2586
  }
2480
- isSupported() {
2481
- return this.isBrowserEnvironment() && 'EventSource' in window;
2587
+ getCapabilityId() {
2588
+ return 'serverSentEvents';
2482
2589
  }
2483
2590
  ensureSSESupported() {
2484
- if (!this.isSupported()) {
2485
- throw new Error('Server-Sent Events (EventSource) not supported in this environment');
2486
- }
2591
+ this.ensureSupported();
2487
2592
  }
2488
2593
  connect(url, config = {}) {
2489
2594
  this.ensureSSESupported();
@@ -2563,8 +2668,8 @@ class VibrationService extends BrowserApiBaseService {
2563
2668
  notification: [200],
2564
2669
  doubleTap: [50, 100, 50],
2565
2670
  };
2566
- isSupported() {
2567
- return this.isBrowserEnvironment() && 'vibrate' in navigator;
2671
+ getCapabilityId() {
2672
+ return 'vibration';
2568
2673
  }
2569
2674
  vibrate(pattern = 200) {
2570
2675
  if (!this.isSupported())
@@ -2597,13 +2702,11 @@ class SpeechSynthesisService extends BrowserApiBaseService {
2597
2702
  getApiName() {
2598
2703
  return 'speech-synthesis';
2599
2704
  }
2600
- isSupported() {
2601
- return this.isBrowserEnvironment() && 'speechSynthesis' in window;
2705
+ getCapabilityId() {
2706
+ return 'speechSynthesis';
2602
2707
  }
2603
2708
  ensureSpeechSynthesisSupported() {
2604
- if (!this.isSupported()) {
2605
- throw new Error('Speech Synthesis API not supported in this browser');
2606
- }
2709
+ this.ensureSupported();
2607
2710
  }
2608
2711
  get state() {
2609
2712
  if (!this.isSupported())
@@ -2713,8 +2816,8 @@ class MutationObserverService extends BrowserApiBaseService {
2713
2816
  getApiName() {
2714
2817
  return 'mutation-observer';
2715
2818
  }
2716
- isSupported() {
2717
- return this.isBrowserEnvironment() && isMutationObserverSupported();
2819
+ getCapabilityId() {
2820
+ return 'mutationObserver';
2718
2821
  }
2719
2822
  observe(target, options) {
2720
2823
  if (!this.isSupported()) {
@@ -2751,8 +2854,8 @@ class PerformanceObserverService extends BrowserApiBaseService {
2751
2854
  getApiName() {
2752
2855
  return 'performance-observer';
2753
2856
  }
2754
- isSupported() {
2755
- return this.isBrowserEnvironment() && isPerformanceObserverSupported();
2857
+ getCapabilityId() {
2858
+ return 'performanceObserver';
2756
2859
  }
2757
2860
  observe(config) {
2758
2861
  if (!this.isSupported()) {
@@ -2780,8 +2883,8 @@ class WebAudioService extends BrowserApiBaseService {
2780
2883
  return 'web-audio';
2781
2884
  }
2782
2885
  context = null;
2783
- isSupported() {
2784
- return this.isBrowserEnvironment() && 'AudioContext' in window;
2886
+ getCapabilityId() {
2887
+ return 'webAudio';
2785
2888
  }
2786
2889
  getContext() {
2787
2890
  if (!this.isSupported()) {
@@ -2942,8 +3045,8 @@ class GamepadService extends BrowserApiBaseService {
2942
3045
  getApiName() {
2943
3046
  return 'gamepad';
2944
3047
  }
2945
- isSupported() {
2946
- return this.isBrowserEnvironment() && isGamepadSupported();
3048
+ getCapabilityId() {
3049
+ return 'gamepad';
2947
3050
  }
2948
3051
  getSnapshot(index) {
2949
3052
  if (!this.isSupported())
@@ -3163,6 +3266,257 @@ function injectGamepad(index, intervalMs = 16) {
3163
3266
  };
3164
3267
  }
3165
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
+
3166
3520
  class BrowserSupportUtil {
3167
3521
  static isSupported(feature) {
3168
3522
  if (typeof window === 'undefined' || typeof navigator === 'undefined') {
@@ -3484,4 +3838,4 @@ const version = '0.1.0';
3484
3838
  * Generated bundle index. Do not edit.
3485
3839
  */
3486
3840
 
3487
- 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 };