@angular-helpers/browser-web-apis 21.7.0 → 21.10.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,191 @@ 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
+ { id: 'webLocks', label: 'Web Locks API', requiresSecureContext: true },
60
+ { id: 'storageManager', label: 'Storage Manager API', requiresSecureContext: true },
61
+ { id: 'compressionStreams', label: 'Compression Streams API', requiresSecureContext: false },
62
+ ];
63
+ class BrowserCapabilityService {
64
+ getCapabilities() {
65
+ return BROWSER_CAPABILITIES;
66
+ }
67
+ isSecureContext() {
68
+ return typeof window !== 'undefined' && window.isSecureContext;
69
+ }
70
+ isSupported(capability) {
71
+ switch (capability) {
72
+ case 'permissions':
73
+ return typeof navigator !== 'undefined' && 'permissions' in navigator;
74
+ case 'geolocation':
75
+ return typeof navigator !== 'undefined' && 'geolocation' in navigator;
76
+ case 'clipboard':
77
+ return typeof navigator !== 'undefined' && 'clipboard' in navigator;
78
+ case 'notification':
79
+ return typeof window !== 'undefined' && 'Notification' in window;
80
+ case 'mediaDevices':
81
+ case 'camera':
82
+ return typeof navigator !== 'undefined' && 'mediaDevices' in navigator;
83
+ case 'webWorker':
84
+ case 'regexSecurity':
85
+ return typeof Worker !== 'undefined';
86
+ case 'webStorage':
87
+ return typeof Storage !== 'undefined';
88
+ case 'webShare':
89
+ return typeof navigator !== 'undefined' && 'share' in navigator;
90
+ case 'battery':
91
+ return typeof navigator !== 'undefined' && 'getBattery' in navigator;
92
+ case 'webSocket':
93
+ return typeof WebSocket !== 'undefined';
94
+ case 'intersectionObserver':
95
+ return typeof IntersectionObserver !== 'undefined';
96
+ case 'resizeObserver':
97
+ return typeof ResizeObserver !== 'undefined';
98
+ case 'pageVisibility':
99
+ return typeof document !== 'undefined' && 'hidden' in document;
100
+ case 'broadcastChannel':
101
+ return typeof BroadcastChannel !== 'undefined';
102
+ case 'networkInformation':
103
+ return (typeof navigator !== 'undefined' &&
104
+ ('connection' in navigator || 'mozConnection' in navigator));
105
+ case 'screenWakeLock':
106
+ return typeof navigator !== 'undefined' && 'wakeLock' in navigator;
107
+ case 'screenOrientation':
108
+ return typeof screen !== 'undefined' && 'orientation' in screen;
109
+ case 'fullscreen':
110
+ return (typeof document !== 'undefined' &&
111
+ ('fullscreenEnabled' in document || 'webkitFullscreenEnabled' in document));
112
+ case 'fileSystemAccess':
113
+ return typeof window !== 'undefined' && 'showOpenFilePicker' in window;
114
+ case 'mediaRecorder':
115
+ return typeof MediaRecorder !== 'undefined';
116
+ case 'serverSentEvents':
117
+ return typeof EventSource !== 'undefined';
118
+ case 'vibration':
119
+ return typeof navigator !== 'undefined' && 'vibrate' in navigator;
120
+ case 'speechSynthesis':
121
+ return typeof window !== 'undefined' && 'speechSynthesis' in window;
122
+ case 'mutationObserver':
123
+ return typeof MutationObserver !== 'undefined';
124
+ case 'performanceObserver':
125
+ return typeof PerformanceObserver !== 'undefined';
126
+ case 'idleDetector':
127
+ return typeof window !== 'undefined' && 'IdleDetector' in window;
128
+ case 'eyeDropper':
129
+ return typeof window !== 'undefined' && 'EyeDropper' in window;
130
+ case 'barcodeDetector':
131
+ return typeof window !== 'undefined' && 'BarcodeDetector' in window;
132
+ case 'webAudio':
133
+ return typeof window !== 'undefined' && 'AudioContext' in window;
134
+ case 'gamepad':
135
+ return typeof navigator !== 'undefined' && 'getGamepads' in navigator;
136
+ case 'webBluetooth':
137
+ return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
138
+ case 'webUsb':
139
+ return typeof navigator !== 'undefined' && 'usb' in navigator;
140
+ case 'webNfc':
141
+ return typeof window !== 'undefined' && 'NDEFReader' in window;
142
+ case 'paymentRequest':
143
+ return typeof window !== 'undefined' && 'PaymentRequest' in window;
144
+ case 'credentialManagement':
145
+ return typeof navigator !== 'undefined' && 'credentials' in navigator;
146
+ case 'webLocks':
147
+ return typeof navigator !== 'undefined' && 'locks' in navigator;
148
+ case 'storageManager':
149
+ return (typeof navigator !== 'undefined' &&
150
+ 'storage' in navigator &&
151
+ typeof navigator.storage.estimate === 'function');
152
+ case 'compressionStreams':
153
+ return (typeof CompressionStream !== 'undefined' && typeof DecompressionStream !== 'undefined');
154
+ default:
155
+ return false;
156
+ }
157
+ }
158
+ getAllStatuses() {
159
+ const secureContext = this.isSecureContext();
160
+ return this.getCapabilities().map((capability) => ({
161
+ id: capability.id,
162
+ label: capability.label,
163
+ supported: this.isSupported(capability.id),
164
+ secureContext,
165
+ requiresSecureContext: capability.requiresSecureContext,
166
+ }));
167
+ }
168
+ async getPermissionState(permission) {
169
+ if (typeof navigator === 'undefined' || !('permissions' in navigator)) {
170
+ return 'unknown';
171
+ }
172
+ try {
173
+ const status = await navigator.permissions.query({ name: permission });
174
+ return status.state;
175
+ }
176
+ catch {
177
+ return 'unknown';
178
+ }
179
+ }
180
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
181
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, providedIn: 'root' });
182
+ }
183
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, decorators: [{
184
+ type: Injectable,
185
+ args: [{ providedIn: 'root' }]
186
+ }] });
187
+
21
188
  /**
22
189
  * 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
190
+ *
191
+ * ## Support detection contract
192
+ *
193
+ * Services follow ONE pattern (do not invent variants):
194
+ *
195
+ * - `isSupported(): boolean` public, side-effect free, SSR-safe. Default
196
+ * implementation delegates to {@link BrowserCapabilityService} when the subclass
197
+ * overrides {@link getCapabilityId} (recommended).
198
+ * - `ensureSupported(): void` — internal Template Method. Throws when called outside
199
+ * browser or when the underlying API is missing.
200
+ *
201
+ * ## Error surfacing contract
202
+ *
203
+ * - **Imperative methods** MUST call `ensureSupported()` and throw synchronously.
204
+ * - **Stream-returning methods** MUST guard with `isSupported()` and surface
205
+ * unsupported state as `Observable.error(...)` (NOT throw inline).
29
206
  *
30
207
  * Services that also need permission querying should extend
31
208
  * `PermissionAwareBrowserApiBaseService` instead.
@@ -34,30 +211,46 @@ class BrowserApiBaseService {
34
211
  destroyRef = inject(DestroyRef);
35
212
  platformId = inject(PLATFORM_ID);
36
213
  logger = inject(BROWSER_API_LOGGER);
214
+ capabilities = inject(BrowserCapabilityService);
37
215
  /**
38
- * Check if running in browser environment using Angular's platform detection.
216
+ * Optional hook for subclasses to delegate feature detection to
217
+ * {@link BrowserCapabilityService}. Returning a capability id removes the need to
218
+ * implement `isSupported()` manually and avoids drift between per-service checks
219
+ * and the centralized capability registry.
39
220
  */
221
+ getCapabilityId() {
222
+ return null;
223
+ }
224
+ /** Public, SSR-safe support check. Override only if you need extra constraints. */
225
+ isSupported() {
226
+ if (!this.isBrowserEnvironment())
227
+ return false;
228
+ const capabilityId = this.getCapabilityId();
229
+ if (capabilityId !== null) {
230
+ return this.capabilities.isSupported(capabilityId);
231
+ }
232
+ return true;
233
+ }
40
234
  isBrowserEnvironment() {
41
235
  return isPlatformBrowser(this.platformId);
42
236
  }
43
- /**
44
- * Check if running in server environment using Angular's platform detection.
45
- */
46
237
  isServerEnvironment() {
47
238
  return isPlatformServer(this.platformId);
48
239
  }
49
240
  /**
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.
241
+ * Template Method: asserts the service can run in the current environment. Subclasses
242
+ * that need extra checks beyond capability detection MUST call `super.ensureSupported()`
243
+ * first, then add their own check.
52
244
  */
53
245
  ensureSupported() {
54
246
  if (!this.isBrowserEnvironment()) {
55
247
  throw new Error(`${this.getApiName()} API not available in server environment`);
56
248
  }
249
+ const capabilityId = this.getCapabilityId();
250
+ if (capabilityId !== null && !this.capabilities.isSupported(capabilityId)) {
251
+ throw new Error(`${this.getApiName()} API not supported in this browser`);
252
+ }
57
253
  }
58
- /**
59
- * Create an error with proper cause chaining.
60
- */
61
254
  createError(message, cause) {
62
255
  const error = new Error(message);
63
256
  if (cause !== undefined) {
@@ -65,21 +258,12 @@ class BrowserApiBaseService {
65
258
  }
66
259
  return error;
67
260
  }
68
- /**
69
- * Log an error through the injected BROWSER_API_LOGGER (default: console).
70
- */
71
261
  logError(message, error) {
72
262
  this.logger.error(`[${this.getApiName()}] ${message}`, error);
73
263
  }
74
- /**
75
- * Log a warning through the injected BROWSER_API_LOGGER (default: console).
76
- */
77
264
  logWarn(message) {
78
265
  this.logger.warn(`[${this.getApiName()}] ${message}`);
79
266
  }
80
- /**
81
- * Log an informational message through the injected BROWSER_API_LOGGER (default: console).
82
- */
83
267
  logInfo(message) {
84
268
  this.logger.info(`[${this.getApiName()}] ${message}`);
85
269
  }
@@ -112,8 +296,8 @@ class PermissionsService extends BrowserApiBaseService {
112
296
  throw error;
113
297
  }
114
298
  }
115
- isSupported() {
116
- return this.isBrowserEnvironment() && 'permissions' in navigator;
299
+ getCapabilityId() {
300
+ return 'permissions';
117
301
  }
118
302
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
119
303
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService });
@@ -127,10 +311,13 @@ class CameraService extends BrowserApiBaseService {
127
311
  getApiName() {
128
312
  return 'camera';
129
313
  }
314
+ getCapabilityId() {
315
+ return 'camera';
316
+ }
130
317
  ensureSupported() {
131
318
  super.ensureSupported();
132
319
  if (!navigator.mediaDevices?.getUserMedia) {
133
- throw new Error('Camera API not supported — a secure context (HTTPS) is required');
320
+ throw new Error('Camera API not supported — getUserMedia missing or HTTPS required');
134
321
  }
135
322
  }
136
323
  async startCamera(constraints) {
@@ -246,11 +433,8 @@ class GeolocationService extends BrowserApiBaseService {
246
433
  getApiName() {
247
434
  return 'geolocation';
248
435
  }
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
- }
436
+ getCapabilityId() {
437
+ return 'geolocation';
254
438
  }
255
439
  getCurrentPosition(options) {
256
440
  this.ensureSupported();
@@ -292,6 +476,9 @@ class MediaDevicesService extends BrowserApiBaseService {
292
476
  getApiName() {
293
477
  return 'media-devices';
294
478
  }
479
+ getCapabilityId() {
480
+ return 'mediaDevices';
481
+ }
295
482
  ensureSupported() {
296
483
  super.ensureSupported();
297
484
  if (!navigator.mediaDevices) {
@@ -420,11 +607,8 @@ class NotificationService extends BrowserApiBaseService {
420
607
  getApiName() {
421
608
  return 'notifications';
422
609
  }
423
- ensureSupported() {
424
- super.ensureSupported();
425
- if (!('Notification' in window)) {
426
- throw new Error('Notifications API not supported in this browser');
427
- }
610
+ getCapabilityId() {
611
+ return 'notification';
428
612
  }
429
613
  get permission() {
430
614
  if (!this.isBrowserEnvironment() || !('Notification' in window)) {
@@ -460,6 +644,9 @@ class ClipboardService extends BrowserApiBaseService {
460
644
  getApiName() {
461
645
  return 'clipboard';
462
646
  }
647
+ getCapabilityId() {
648
+ return 'clipboard';
649
+ }
463
650
  ensureSupported() {
464
651
  super.ensureSupported();
465
652
  if (!navigator.clipboard) {
@@ -493,173 +680,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
493
680
  type: Injectable
494
681
  }] });
495
682
 
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
683
  class BatteryService extends BrowserApiBaseService {
653
684
  batteryManager = null;
654
685
  getApiName() {
655
686
  return 'battery';
656
687
  }
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
- }
688
+ getCapabilityId() {
689
+ return 'battery';
663
690
  }
664
691
  async initialize() {
665
692
  this.ensureSupported();
@@ -739,11 +766,8 @@ class WebShareService extends BrowserApiBaseService {
739
766
  getApiName() {
740
767
  return 'web-share';
741
768
  }
742
- ensureSupported() {
743
- super.ensureSupported();
744
- if (!('share' in navigator)) {
745
- throw new Error('Web Share API not supported in this browser');
746
- }
769
+ getCapabilityId() {
770
+ return 'webShare';
747
771
  }
748
772
  async share(data) {
749
773
  this.ensureSupported();
@@ -998,11 +1022,8 @@ class WebStorageService extends BrowserApiBaseService {
998
1022
  getApiName() {
999
1023
  return 'storage';
1000
1024
  }
1001
- ensureSupported() {
1002
- super.ensureSupported();
1003
- if (typeof Storage === 'undefined') {
1004
- throw new Error('Storage API not supported in this browser');
1005
- }
1025
+ getCapabilityId() {
1026
+ return 'webStorage';
1006
1027
  }
1007
1028
  /** Returns true if either local or session storage is usable. */
1008
1029
  isSupported() {
@@ -1122,7 +1143,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
1122
1143
  }], ctorParameters: () => [] });
1123
1144
 
1124
1145
  const DEFAULT_MAX_RECONNECT_DELAY = 30_000;
1125
- const DEFAULT_REQUEST_TIMEOUT = 30_000;
1146
+ const DEFAULT_REQUEST_TIMEOUT$1 = 30_000;
1126
1147
  /**
1127
1148
  * Stateful WebSocket client wrapping a single connection. One instance per logical
1128
1149
  * connection (do NOT share between `connect()` calls).
@@ -1194,7 +1215,7 @@ class WebSocketClient {
1194
1215
  */
1195
1216
  request(type, data, opts) {
1196
1217
  const id = this.generateId();
1197
- const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
1218
+ const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT$1;
1198
1219
  return new Promise((resolve, reject) => {
1199
1220
  const timer = setTimeout(() => {
1200
1221
  this.pendingRequests.delete(id);
@@ -1406,11 +1427,8 @@ class WebSocketService extends BrowserApiBaseService {
1406
1427
  getApiName() {
1407
1428
  return 'websocket';
1408
1429
  }
1409
- ensureSupported() {
1410
- super.ensureSupported();
1411
- if (typeof WebSocket === 'undefined') {
1412
- throw new Error('WebSocket API not supported in this browser');
1413
- }
1430
+ getCapabilityId() {
1431
+ return 'webSocket';
1414
1432
  }
1415
1433
  /**
1416
1434
  * Create a new WebSocket client. The client owns one connection and is the recommended
@@ -1568,162 +1586,259 @@ function toObservableLike(client) {
1568
1586
  });
1569
1587
  }
1570
1588
 
1589
+ const DEFAULT_REQUEST_TIMEOUT = 30_000;
1590
+ /**
1591
+ * Service for creating and managing Web Workers with first-class support for
1592
+ * request/response over `postMessage` (id correlation, timeout, transferables).
1593
+ *
1594
+ * Status is exposed both as a `Signal<WorkerStatus>` (preferred via
1595
+ * {@link getStatusSignal}) and as an `Observable<WorkerStatus>` for backward
1596
+ * compatibility ({@link createWorker}, {@link getStatus}).
1597
+ *
1598
+ * The service registers a single `DestroyRef.onDestroy` in its constructor that
1599
+ * terminates every registered worker; per-worker handlers do not register their
1600
+ * own cleanup callbacks (avoids accumulating callbacks per `setupWorker` call,
1601
+ * a previous leak source).
1602
+ */
1571
1603
  class WebWorkerService extends BrowserApiBaseService {
1572
- workers = new Map();
1573
- workerStatuses = new Map();
1574
- workerMessages = new Map();
1575
- currentWorkerStatuses = new Map();
1604
+ workerLogger = inject(BROWSER_API_LOGGER);
1605
+ entries = new Map();
1576
1606
  _cleanup = this.destroyRef.onDestroy(() => this.terminateAllWorkers());
1577
1607
  getApiName() {
1578
1608
  return 'webworker';
1579
1609
  }
1580
- ensureSupported() {
1581
- super.ensureSupported();
1582
- if (typeof Worker === 'undefined') {
1583
- throw new Error('Web Workers not supported in this browser');
1584
- }
1610
+ getCapabilityId() {
1611
+ return 'webWorker';
1585
1612
  }
1613
+ /**
1614
+ * Create a worker. Idempotent: calling twice with the same name returns the
1615
+ * existing entry without recreating the worker.
1616
+ *
1617
+ * Returns an `Observable<WorkerStatus>` for backward compatibility. Prefer
1618
+ * {@link createWorkerSignal} for new code.
1619
+ */
1586
1620
  createWorker(name, scriptUrl) {
1621
+ const status = this.createWorkerSignal(name, scriptUrl);
1622
+ return toObservable(status);
1623
+ }
1624
+ /**
1625
+ * Create a worker (signal-first). Returns the status signal; status is also
1626
+ * accessible later via {@link getStatusSignal}.
1627
+ */
1628
+ createWorkerSignal(name, scriptUrl) {
1587
1629
  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
- });
1630
+ const existing = this.entries.get(name);
1631
+ if (existing && existing.worker)
1632
+ return existing.status.asReadonly();
1633
+ const entry = this.ensureEntry(name);
1634
+ try {
1635
+ const worker = new Worker(scriptUrl);
1636
+ entry.worker = worker;
1637
+ this.attachHandlers(name, entry);
1638
+ entry.status.set({ initialized: true, running: true, messageCount: 0 });
1639
+ }
1640
+ catch (error) {
1641
+ const message = error instanceof Error ? error.message : 'Unknown error';
1642
+ entry.status.set({
1643
+ initialized: false,
1644
+ running: false,
1645
+ error: message,
1646
+ messageCount: 0,
1647
+ });
1648
+ this.workerLogger.error(`[webworker] Failed to create worker "${name}"`, error);
1649
+ }
1650
+ return entry.status.asReadonly();
1624
1651
  }
1625
1652
  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
- }
1653
+ const entry = this.entries.get(name);
1654
+ if (!entry)
1655
+ return;
1656
+ this.rejectPending(entry, new Error(`Worker "${name}" terminated`));
1657
+ if (entry.worker)
1658
+ entry.worker.terminate();
1659
+ entry.messages$.complete();
1660
+ this.entries.delete(name);
1634
1661
  }
1635
1662
  terminateAllWorkers() {
1636
- this.workers.forEach((_, name) => {
1663
+ for (const name of [...this.entries.keys()]) {
1637
1664
  this.terminateWorker(name);
1638
- });
1665
+ }
1639
1666
  }
1667
+ /** Send a fire-and-forget message. Use {@link request} when you need a reply. */
1640
1668
  postMessage(workerName, task) {
1641
- const worker = this.workers.get(workerName);
1642
- if (!worker) {
1643
- this.logError(`Worker ${workerName} not found`);
1669
+ const entry = this.entries.get(workerName);
1670
+ if (!entry || !entry.worker) {
1671
+ this.workerLogger.error(`[webworker] postMessage: worker "${workerName}" not found`);
1644
1672
  return;
1645
1673
  }
1674
+ const message = {
1675
+ id: task.id ?? this.generateId(),
1676
+ type: task.type,
1677
+ data: task.data,
1678
+ timestamp: Date.now(),
1679
+ };
1646
1680
  try {
1647
- const message = { ...task, timestamp: Date.now() };
1648
1681
  if (task.transferable) {
1649
- worker.postMessage(message, task.transferable);
1682
+ entry.worker.postMessage(message, task.transferable);
1650
1683
  }
1651
1684
  else {
1652
- worker.postMessage(message);
1653
- }
1654
- const currentStatus = this.currentWorkerStatuses.get(workerName);
1655
- if (currentStatus) {
1656
- currentStatus.messageCount++;
1657
- this.updateWorkerStatus(workerName, currentStatus);
1685
+ entry.worker.postMessage(message);
1658
1686
  }
1687
+ this.bumpMessageCount(entry);
1659
1688
  }
1660
1689
  catch (error) {
1661
- this.logError(`Failed to post message to worker ${workerName}:`, error);
1690
+ this.workerLogger.error(`[webworker] postMessage failed for "${workerName}"`, error);
1662
1691
  }
1663
1692
  }
1664
- getMessages(workerName) {
1665
- if (!this.workerMessages.has(workerName)) {
1666
- this.workerMessages.set(workerName, new Subject());
1693
+ /**
1694
+ * Send a message and await a correlated response. The worker MUST send back a
1695
+ * message containing `correlationId` matching the request id.
1696
+ *
1697
+ * ```ts
1698
+ * const result = await ws.request<{ ok: boolean }>('worker', 'compute', { n: 1 });
1699
+ * ```
1700
+ */
1701
+ request(workerName, type, data, opts) {
1702
+ const entry = this.entries.get(workerName);
1703
+ if (!entry || !entry.worker) {
1704
+ return Promise.reject(new Error(`Worker "${workerName}" not found`));
1667
1705
  }
1668
- return this.workerMessages.get(workerName).asObservable();
1706
+ const id = this.generateId();
1707
+ const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
1708
+ return new Promise((resolve, reject) => {
1709
+ const timer = setTimeout(() => {
1710
+ entry.pending.delete(id);
1711
+ reject(new Error(`WebWorker "${workerName}" request timeout after ${timeoutMs}ms`));
1712
+ }, timeoutMs);
1713
+ entry.pending.set(id, {
1714
+ resolve: resolve,
1715
+ reject,
1716
+ timer,
1717
+ });
1718
+ const message = {
1719
+ id,
1720
+ type,
1721
+ data,
1722
+ timestamp: Date.now(),
1723
+ correlationId: id,
1724
+ };
1725
+ try {
1726
+ if (opts?.transferable) {
1727
+ entry.worker.postMessage(message, opts.transferable);
1728
+ }
1729
+ else {
1730
+ entry.worker.postMessage(message);
1731
+ }
1732
+ this.bumpMessageCount(entry);
1733
+ }
1734
+ catch (error) {
1735
+ clearTimeout(timer);
1736
+ entry.pending.delete(id);
1737
+ reject(error instanceof Error ? error : new Error('postMessage failed'));
1738
+ }
1739
+ });
1740
+ }
1741
+ getMessages(workerName) {
1742
+ const entry = this.ensureEntry(workerName);
1743
+ return entry.messages$.asObservable();
1744
+ }
1745
+ getMessagesByType(workerName, type) {
1746
+ return this.getMessages(workerName).pipe(filter((m) => m.type === type));
1669
1747
  }
1748
+ /** @deprecated Use {@link getStatusSignal}. Kept as Observable for backward compat. */
1670
1749
  getStatus(workerName) {
1671
- if (!this.workerStatuses.has(workerName)) {
1672
- this.workerStatuses.set(workerName, new Subject());
1673
- }
1674
- return this.workerStatuses.get(workerName).asObservable();
1750
+ return toObservable(this.getStatusSignal(workerName));
1751
+ }
1752
+ getStatusSignal(workerName) {
1753
+ return this.ensureEntry(workerName).status.asReadonly();
1675
1754
  }
1676
1755
  getCurrentStatus(workerName) {
1677
- return this.currentWorkerStatuses.get(workerName);
1756
+ return this.entries.get(workerName)?.status();
1678
1757
  }
1679
1758
  getAllStatuses() {
1680
- return new Map(this.currentWorkerStatuses);
1759
+ const result = new Map();
1760
+ for (const [name, entry] of this.entries)
1761
+ result.set(name, entry.status());
1762
+ return result;
1681
1763
  }
1682
1764
  isWorkerRunning(workerName) {
1683
- const status = this.currentWorkerStatuses.get(workerName);
1684
- return status?.running ?? false;
1765
+ return this.entries.get(workerName)?.status().running ?? false;
1766
+ }
1767
+ getNativeWorker(name) {
1768
+ return this.entries.get(name)?.worker ?? undefined;
1685
1769
  }
1686
- setupWorker(name, worker) {
1687
- worker.onmessage = (event) => {
1770
+ getAllWorkers() {
1771
+ const result = new Map();
1772
+ for (const [name, entry] of this.entries) {
1773
+ if (entry.worker)
1774
+ result.set(name, entry.worker);
1775
+ }
1776
+ return result;
1777
+ }
1778
+ // ---------- internals ----------
1779
+ attachHandlers(name, entry) {
1780
+ if (!entry.worker)
1781
+ return;
1782
+ entry.worker.onmessage = (event) => {
1783
+ const data = event.data ?? {};
1784
+ const correlationId = data.correlationId ?? data.id;
1785
+ if (correlationId && entry.pending.has(correlationId)) {
1786
+ const p = entry.pending.get(correlationId);
1787
+ clearTimeout(p.timer);
1788
+ entry.pending.delete(correlationId);
1789
+ p.resolve(data.data);
1790
+ return;
1791
+ }
1688
1792
  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(),
1793
+ id: data.id ?? this.generateId(),
1794
+ type: data.type ?? 'message',
1795
+ data: data.data,
1796
+ timestamp: data.timestamp ?? Date.now(),
1797
+ correlationId: data.correlationId,
1693
1798
  };
1694
- if (!this.workerMessages.has(name)) {
1695
- this.workerMessages.set(name, new Subject());
1696
- }
1697
- this.workerMessages.get(name).next(message);
1799
+ entry.messages$.next(message);
1698
1800
  };
1699
- worker.onerror = (error) => {
1700
- this.logError(`Worker ${name} error:`, error);
1701
- const status = {
1702
- initialized: true,
1801
+ entry.worker.onerror = (error) => {
1802
+ this.workerLogger.error(`[webworker] Worker "${name}" error`, error);
1803
+ entry.status.update((s) => ({
1804
+ ...s,
1703
1805
  running: false,
1704
1806
  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);
1807
+ }));
1709
1808
  };
1710
- // Auto-cleanup when service is destroyed
1711
- this.destroyRef.onDestroy(() => {
1712
- this.terminateWorker(name);
1713
- });
1714
1809
  }
1715
- updateWorkerStatus(name, status) {
1716
- if (!this.workerStatuses.has(name)) {
1717
- this.workerStatuses.set(name, new Subject());
1810
+ rejectPending(entry, reason) {
1811
+ for (const p of entry.pending.values()) {
1812
+ clearTimeout(p.timer);
1813
+ p.reject(reason);
1718
1814
  }
1719
- this.workerStatuses.get(name).next(status);
1815
+ entry.pending.clear();
1720
1816
  }
1721
- // Direct access to native Worker API
1722
- getNativeWorker(name) {
1723
- return this.workers.get(name);
1817
+ bumpMessageCount(entry) {
1818
+ entry.status.update((s) => ({ ...s, messageCount: s.messageCount + 1 }));
1724
1819
  }
1725
- getAllWorkers() {
1726
- return new Map(this.workers);
1820
+ ensureEntry(workerName) {
1821
+ let entry = this.entries.get(workerName);
1822
+ if (!entry) {
1823
+ entry = {
1824
+ worker: null,
1825
+ status: signal({
1826
+ initialized: false,
1827
+ running: false,
1828
+ messageCount: 0,
1829
+ }),
1830
+ messages$: new Subject(),
1831
+ pending: new Map(),
1832
+ };
1833
+ this.entries.set(workerName, entry);
1834
+ }
1835
+ return entry;
1836
+ }
1837
+ generateId() {
1838
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
1839
+ return globalThis.crypto.randomUUID();
1840
+ }
1841
+ return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
1727
1842
  }
1728
1843
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1729
1844
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService });
@@ -1763,8 +1878,8 @@ class IntersectionObserverService extends BrowserApiBaseService {
1763
1878
  getApiName() {
1764
1879
  return 'intersection-observer';
1765
1880
  }
1766
- isSupported() {
1767
- return this.isBrowserEnvironment() && 'IntersectionObserver' in window;
1881
+ getCapabilityId() {
1882
+ return 'intersectionObserver';
1768
1883
  }
1769
1884
  observe(element, options = {}) {
1770
1885
  if (!this.isSupported()) {
@@ -1823,8 +1938,8 @@ class ResizeObserverService extends BrowserApiBaseService {
1823
1938
  getApiName() {
1824
1939
  return 'resize-observer';
1825
1940
  }
1826
- isSupported() {
1827
- return this.isBrowserEnvironment() && 'ResizeObserver' in window;
1941
+ getCapabilityId() {
1942
+ return 'resizeObserver';
1828
1943
  }
1829
1944
  observe(element, options = {}) {
1830
1945
  if (!this.isSupported()) {
@@ -1864,8 +1979,8 @@ class PageVisibilityService extends BrowserApiBaseService {
1864
1979
  getApiName() {
1865
1980
  return 'page-visibility';
1866
1981
  }
1867
- isSupported() {
1868
- return this.isBrowserEnvironment() && 'hidden' in document;
1982
+ getCapabilityId() {
1983
+ return 'pageVisibility';
1869
1984
  }
1870
1985
  get isHidden() {
1871
1986
  if (!this.isSupported())
@@ -1941,13 +2056,11 @@ class BroadcastChannelService extends ConnectionRegistryBaseService {
1941
2056
  closeNativeConnection(channel) {
1942
2057
  channel.close();
1943
2058
  }
1944
- isSupported() {
1945
- return this.isBrowserEnvironment() && 'BroadcastChannel' in window;
2059
+ getCapabilityId() {
2060
+ return 'broadcastChannel';
1946
2061
  }
1947
2062
  ensureBroadcastChannelSupported() {
1948
- if (!this.isSupported()) {
1949
- throw new Error('BroadcastChannel API not supported in this environment');
1950
- }
2063
+ this.ensureSupported();
1951
2064
  }
1952
2065
  open(name) {
1953
2066
  this.ensureBroadcastChannelSupported();
@@ -2044,8 +2157,8 @@ class NetworkInformationService extends BrowserApiBaseService {
2044
2157
  getApiName() {
2045
2158
  return 'network-information';
2046
2159
  }
2047
- isSupported() {
2048
- return this.isBrowserEnvironment() && isNetworkInformationSupported();
2160
+ getCapabilityId() {
2161
+ return 'networkInformation';
2049
2162
  }
2050
2163
  getSnapshot() {
2051
2164
  return this.isBrowserEnvironment() ? getNetworkSnapshot() : { online: true };
@@ -2068,8 +2181,8 @@ class ScreenWakeLockService extends BrowserApiBaseService {
2068
2181
  getApiName() {
2069
2182
  return 'screen-wake-lock';
2070
2183
  }
2071
- isSupported() {
2072
- return this.isBrowserEnvironment() && 'wakeLock' in navigator;
2184
+ getCapabilityId() {
2185
+ return 'screenWakeLock';
2073
2186
  }
2074
2187
  get isActive() {
2075
2188
  return this.sentinel !== null && !this.sentinel.released;
@@ -2163,8 +2276,8 @@ class ScreenOrientationService extends BrowserApiBaseService {
2163
2276
  getApiName() {
2164
2277
  return 'screen-orientation';
2165
2278
  }
2166
- isSupported() {
2167
- return this.isBrowserEnvironment() && 'screen' in window && 'orientation' in screen;
2279
+ getCapabilityId() {
2280
+ return 'screenOrientation';
2168
2281
  }
2169
2282
  getSnapshot() {
2170
2283
  return this.isBrowserEnvironment()
@@ -2208,8 +2321,12 @@ class FullscreenService extends BrowserApiBaseService {
2208
2321
  getApiName() {
2209
2322
  return 'fullscreen';
2210
2323
  }
2324
+ getCapabilityId() {
2325
+ return 'fullscreen';
2326
+ }
2327
+ /** Override to also check the *enabled* flag (browser may have disabled fullscreen). */
2211
2328
  isSupported() {
2212
- if (!this.isBrowserEnvironment())
2329
+ if (!super.isSupported())
2213
2330
  return false;
2214
2331
  return !!(document.fullscreenEnabled ??
2215
2332
  document.webkitFullscreenEnabled);
@@ -2300,17 +2417,18 @@ class FileSystemAccessService extends BrowserApiBaseService {
2300
2417
  getApiName() {
2301
2418
  return 'file-system-access';
2302
2419
  }
2420
+ getCapabilityId() {
2421
+ return 'fileSystemAccess';
2422
+ }
2423
+ /** Override to also assert secure context (required by the spec). */
2303
2424
  isSupported() {
2304
- return this.isBrowserEnvironment() && 'showOpenFilePicker' in window && window.isSecureContext;
2425
+ return super.isSupported() && typeof window !== 'undefined' && window.isSecureContext;
2305
2426
  }
2306
2427
  get win() {
2307
2428
  return window;
2308
2429
  }
2309
2430
  ensureSupported() {
2310
2431
  super.ensureSupported();
2311
- if (!('showOpenFilePicker' in window)) {
2312
- throw new Error('File System Access API not supported in this browser');
2313
- }
2314
2432
  if (!window.isSecureContext) {
2315
2433
  throw new Error('File System Access API requires a secure context (HTTPS)');
2316
2434
  }
@@ -2386,8 +2504,8 @@ class MediaRecorderService extends BrowserApiBaseService {
2386
2504
  getApiName() {
2387
2505
  return 'media-recorder';
2388
2506
  }
2389
- isSupported() {
2390
- return this.isBrowserEnvironment() && 'MediaRecorder' in window;
2507
+ getCapabilityId() {
2508
+ return 'mediaRecorder';
2391
2509
  }
2392
2510
  get state() {
2393
2511
  return this.recorder?.state ?? 'inactive';
@@ -2477,13 +2595,11 @@ class ServerSentEventsService extends ConnectionRegistryBaseService {
2477
2595
  closeNativeConnection(source) {
2478
2596
  source.close();
2479
2597
  }
2480
- isSupported() {
2481
- return this.isBrowserEnvironment() && 'EventSource' in window;
2598
+ getCapabilityId() {
2599
+ return 'serverSentEvents';
2482
2600
  }
2483
2601
  ensureSSESupported() {
2484
- if (!this.isSupported()) {
2485
- throw new Error('Server-Sent Events (EventSource) not supported in this environment');
2486
- }
2602
+ this.ensureSupported();
2487
2603
  }
2488
2604
  connect(url, config = {}) {
2489
2605
  this.ensureSSESupported();
@@ -2563,8 +2679,8 @@ class VibrationService extends BrowserApiBaseService {
2563
2679
  notification: [200],
2564
2680
  doubleTap: [50, 100, 50],
2565
2681
  };
2566
- isSupported() {
2567
- return this.isBrowserEnvironment() && 'vibrate' in navigator;
2682
+ getCapabilityId() {
2683
+ return 'vibration';
2568
2684
  }
2569
2685
  vibrate(pattern = 200) {
2570
2686
  if (!this.isSupported())
@@ -2597,13 +2713,11 @@ class SpeechSynthesisService extends BrowserApiBaseService {
2597
2713
  getApiName() {
2598
2714
  return 'speech-synthesis';
2599
2715
  }
2600
- isSupported() {
2601
- return this.isBrowserEnvironment() && 'speechSynthesis' in window;
2716
+ getCapabilityId() {
2717
+ return 'speechSynthesis';
2602
2718
  }
2603
2719
  ensureSpeechSynthesisSupported() {
2604
- if (!this.isSupported()) {
2605
- throw new Error('Speech Synthesis API not supported in this browser');
2606
- }
2720
+ this.ensureSupported();
2607
2721
  }
2608
2722
  get state() {
2609
2723
  if (!this.isSupported())
@@ -2713,8 +2827,8 @@ class MutationObserverService extends BrowserApiBaseService {
2713
2827
  getApiName() {
2714
2828
  return 'mutation-observer';
2715
2829
  }
2716
- isSupported() {
2717
- return this.isBrowserEnvironment() && isMutationObserverSupported();
2830
+ getCapabilityId() {
2831
+ return 'mutationObserver';
2718
2832
  }
2719
2833
  observe(target, options) {
2720
2834
  if (!this.isSupported()) {
@@ -2751,8 +2865,8 @@ class PerformanceObserverService extends BrowserApiBaseService {
2751
2865
  getApiName() {
2752
2866
  return 'performance-observer';
2753
2867
  }
2754
- isSupported() {
2755
- return this.isBrowserEnvironment() && isPerformanceObserverSupported();
2868
+ getCapabilityId() {
2869
+ return 'performanceObserver';
2756
2870
  }
2757
2871
  observe(config) {
2758
2872
  if (!this.isSupported()) {
@@ -2780,8 +2894,8 @@ class WebAudioService extends BrowserApiBaseService {
2780
2894
  return 'web-audio';
2781
2895
  }
2782
2896
  context = null;
2783
- isSupported() {
2784
- return this.isBrowserEnvironment() && 'AudioContext' in window;
2897
+ getCapabilityId() {
2898
+ return 'webAudio';
2785
2899
  }
2786
2900
  getContext() {
2787
2901
  if (!this.isSupported()) {
@@ -2942,8 +3056,8 @@ class GamepadService extends BrowserApiBaseService {
2942
3056
  getApiName() {
2943
3057
  return 'gamepad';
2944
3058
  }
2945
- isSupported() {
2946
- return this.isBrowserEnvironment() && isGamepadSupported();
3059
+ getCapabilityId() {
3060
+ return 'gamepad';
2947
3061
  }
2948
3062
  getSnapshot(index) {
2949
3063
  if (!this.isSupported())
@@ -2984,6 +3098,193 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
2984
3098
  type: Injectable
2985
3099
  }] });
2986
3100
 
3101
+ /**
3102
+ * Service wrapping `navigator.locks` (Web Locks API). Coordinates exclusive or
3103
+ * shared access to a named resource across tabs and workers.
3104
+ *
3105
+ * ```ts
3106
+ * const locks = inject(WebLocksService);
3107
+ * await locks.acquire('user-cache', async () => {
3108
+ * // critical section — no other tab is in this block at the same time
3109
+ * });
3110
+ * ```
3111
+ */
3112
+ class WebLocksService extends BrowserApiBaseService {
3113
+ getApiName() {
3114
+ return 'web-locks';
3115
+ }
3116
+ isSupported() {
3117
+ if (!this.isBrowserEnvironment())
3118
+ return false;
3119
+ return !!navigator.locks;
3120
+ }
3121
+ /**
3122
+ * Acquire a lock and run the callback while holding it. The lock is released
3123
+ * automatically when the callback resolves or rejects.
3124
+ */
3125
+ acquire(name, callback, options = {}) {
3126
+ this.ensureSupported();
3127
+ const nav = navigator;
3128
+ if (Object.keys(options).length === 0) {
3129
+ return nav.locks.request(name, () => callback());
3130
+ }
3131
+ return nav.locks.request(name, options, () => callback());
3132
+ }
3133
+ /**
3134
+ * Query the current lock state. Useful for diagnostics and tests; do not gate
3135
+ * critical-section logic on this — it's a snapshot, not a reservation.
3136
+ */
3137
+ query() {
3138
+ this.ensureSupported();
3139
+ return navigator.locks.query();
3140
+ }
3141
+ ensureSupported() {
3142
+ super.ensureSupported();
3143
+ if (!this.isSupported()) {
3144
+ throw new Error('Web Locks API not supported in this browser');
3145
+ }
3146
+ }
3147
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebLocksService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
3148
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebLocksService });
3149
+ }
3150
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebLocksService, decorators: [{
3151
+ type: Injectable
3152
+ }] });
3153
+
3154
+ /**
3155
+ * Service wrapping `navigator.storage` (StorageManager API). Exposes:
3156
+ * - `estimate()`: how much origin storage is in use vs available
3157
+ * - `persist()`: ask the browser to make storage persistent (eviction-protected)
3158
+ * - `persisted()`: check whether storage is currently persistent
3159
+ */
3160
+ class StorageManagerService extends BrowserApiBaseService {
3161
+ getApiName() {
3162
+ return 'storage-manager';
3163
+ }
3164
+ isSupported() {
3165
+ if (!this.isBrowserEnvironment())
3166
+ return false;
3167
+ const sm = navigator.storage;
3168
+ return !!sm && typeof sm.estimate === 'function';
3169
+ }
3170
+ async estimate() {
3171
+ this.ensureSupported();
3172
+ const sm = navigator.storage;
3173
+ const result = await sm.estimate();
3174
+ return {
3175
+ usage: result.usage ?? 0,
3176
+ quota: result.quota ?? 0,
3177
+ usageDetails: result
3178
+ .usageDetails,
3179
+ };
3180
+ }
3181
+ async persist() {
3182
+ this.ensureSupported();
3183
+ const sm = navigator.storage;
3184
+ if (typeof sm.persist !== 'function')
3185
+ return false;
3186
+ return sm.persist();
3187
+ }
3188
+ async persisted() {
3189
+ this.ensureSupported();
3190
+ const sm = navigator.storage;
3191
+ if (typeof sm.persisted !== 'function')
3192
+ return false;
3193
+ return sm.persisted();
3194
+ }
3195
+ ensureSupported() {
3196
+ super.ensureSupported();
3197
+ if (!this.isSupported()) {
3198
+ throw new Error('StorageManager API not supported in this browser');
3199
+ }
3200
+ }
3201
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: StorageManagerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
3202
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: StorageManagerService });
3203
+ }
3204
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: StorageManagerService, decorators: [{
3205
+ type: Injectable
3206
+ }] });
3207
+
3208
+ /**
3209
+ * Service wrapping `CompressionStream` and `DecompressionStream`.
3210
+ *
3211
+ * ```ts
3212
+ * const cmp = inject(CompressionService);
3213
+ * const compressed = await cmp.compress(jsonBytes, 'gzip');
3214
+ * const original = await cmp.decompress(compressed, 'gzip');
3215
+ * ```
3216
+ */
3217
+ class CompressionService extends BrowserApiBaseService {
3218
+ getApiName() {
3219
+ return 'compression-streams';
3220
+ }
3221
+ isSupported() {
3222
+ if (!this.isBrowserEnvironment())
3223
+ return false;
3224
+ const g = globalThis;
3225
+ return typeof g.CompressionStream === 'function' && typeof g.DecompressionStream === 'function';
3226
+ }
3227
+ /** Compress a `Uint8Array`/`ArrayBuffer` using the given format. */
3228
+ async compress(data, format = 'gzip') {
3229
+ this.ensureSupported();
3230
+ const g = globalThis;
3231
+ const stream = new g.CompressionStream(format);
3232
+ return this.runStream(data, stream);
3233
+ }
3234
+ /** Decompress a `Uint8Array`/`ArrayBuffer` using the given format. */
3235
+ async decompress(data, format = 'gzip') {
3236
+ this.ensureSupported();
3237
+ const g = globalThis;
3238
+ const stream = new g.DecompressionStream(format);
3239
+ return this.runStream(data, stream);
3240
+ }
3241
+ /** Convenience: compress a UTF-8 string and return the compressed bytes. */
3242
+ async compressString(value, format = 'gzip') {
3243
+ return this.compress(new TextEncoder().encode(value), format);
3244
+ }
3245
+ /** Convenience: decompress bytes into a UTF-8 string. */
3246
+ async decompressString(data, format = 'gzip') {
3247
+ const bytes = await this.decompress(data, format);
3248
+ return new TextDecoder().decode(bytes);
3249
+ }
3250
+ ensureSupported() {
3251
+ super.ensureSupported();
3252
+ if (!this.isSupported()) {
3253
+ throw new Error('Compression Streams API not supported in this browser');
3254
+ }
3255
+ }
3256
+ async runStream(data, stream) {
3257
+ const writer = stream.writable.getWriter();
3258
+ const writePromise = writer.write(data instanceof Uint8Array ? data : new Uint8Array(data));
3259
+ const closePromise = writer.close();
3260
+ const reader = stream.readable.getReader();
3261
+ const chunks = [];
3262
+ let total = 0;
3263
+ while (true) {
3264
+ const { done, value } = await reader.read();
3265
+ if (done)
3266
+ break;
3267
+ const chunk = value;
3268
+ chunks.push(chunk);
3269
+ total += chunk.byteLength;
3270
+ }
3271
+ await writePromise;
3272
+ await closePromise;
3273
+ const result = new Uint8Array(total);
3274
+ let offset = 0;
3275
+ for (const chunk of chunks) {
3276
+ result.set(chunk, offset);
3277
+ offset += chunk.byteLength;
3278
+ }
3279
+ return result;
3280
+ }
3281
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CompressionService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
3282
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CompressionService });
3283
+ }
3284
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CompressionService, decorators: [{
3285
+ type: Injectable
3286
+ }] });
3287
+
2987
3288
  // Common types for browser APIs
2988
3289
 
2989
3290
  function injectPageVisibility() {
@@ -3163,6 +3464,257 @@ function injectGamepad(index, intervalMs = 16) {
3163
3464
  };
3164
3465
  }
3165
3466
 
3467
+ function injectClipboard() {
3468
+ const destroyRef = inject(DestroyRef);
3469
+ const platformId = inject(PLATFORM_ID);
3470
+ const isBrowser = isPlatformBrowser(platformId);
3471
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && !!navigator.clipboard, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3472
+ const text = signal(null, ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
3473
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3474
+ const busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
3475
+ let disposed = false;
3476
+ destroyRef.onDestroy(() => {
3477
+ disposed = true;
3478
+ });
3479
+ const writeText = async (value) => {
3480
+ if (!supported() || disposed) {
3481
+ error.set('Clipboard API not supported');
3482
+ return false;
3483
+ }
3484
+ busy.set(true);
3485
+ try {
3486
+ await navigator.clipboard.writeText(value);
3487
+ if (!disposed) {
3488
+ text.set(value);
3489
+ error.set(null);
3490
+ }
3491
+ return true;
3492
+ }
3493
+ catch (err) {
3494
+ if (!disposed)
3495
+ error.set(err instanceof Error ? err.message : 'writeText failed');
3496
+ return false;
3497
+ }
3498
+ finally {
3499
+ if (!disposed)
3500
+ busy.set(false);
3501
+ }
3502
+ };
3503
+ const readText = async () => {
3504
+ if (!supported() || disposed) {
3505
+ error.set('Clipboard API not supported');
3506
+ return null;
3507
+ }
3508
+ busy.set(true);
3509
+ try {
3510
+ const value = await navigator.clipboard.readText();
3511
+ if (!disposed) {
3512
+ text.set(value);
3513
+ error.set(null);
3514
+ }
3515
+ return value;
3516
+ }
3517
+ catch (err) {
3518
+ if (!disposed)
3519
+ error.set(err instanceof Error ? err.message : 'readText failed');
3520
+ return null;
3521
+ }
3522
+ finally {
3523
+ if (!disposed)
3524
+ busy.set(false);
3525
+ }
3526
+ };
3527
+ return {
3528
+ text: text.asReadonly(),
3529
+ error: error.asReadonly(),
3530
+ busy: busy.asReadonly(),
3531
+ isSupported: supported.asReadonly(),
3532
+ writeText,
3533
+ readText,
3534
+ };
3535
+ }
3536
+
3537
+ function injectGeolocation(opts = {}) {
3538
+ const destroyRef = inject(DestroyRef);
3539
+ const platformId = inject(PLATFORM_ID);
3540
+ const isBrowser = isPlatformBrowser(platformId);
3541
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'geolocation' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3542
+ const position = signal(null, ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
3543
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3544
+ const watching = signal(false, ...(ngDevMode ? [{ debugName: "watching" }] : /* istanbul ignore next */ []));
3545
+ let watchId = null;
3546
+ const watch = (positionOpts) => {
3547
+ if (!supported() || watchId !== null)
3548
+ return;
3549
+ watching.set(true);
3550
+ watchId = navigator.geolocation.watchPosition((pos) => {
3551
+ position.set(pos);
3552
+ error.set(null);
3553
+ }, (err) => error.set(err), positionOpts);
3554
+ };
3555
+ const stop = () => {
3556
+ if (watchId !== null) {
3557
+ navigator.geolocation.clearWatch(watchId);
3558
+ watchId = null;
3559
+ }
3560
+ watching.set(false);
3561
+ };
3562
+ const getCurrent = (positionOpts) => {
3563
+ if (!supported()) {
3564
+ return Promise.reject(new Error('Geolocation API not supported'));
3565
+ }
3566
+ return new Promise((resolve, reject) => {
3567
+ navigator.geolocation.getCurrentPosition((pos) => {
3568
+ position.set(pos);
3569
+ error.set(null);
3570
+ resolve(pos);
3571
+ }, (err) => {
3572
+ error.set(err);
3573
+ reject(err);
3574
+ }, positionOpts);
3575
+ });
3576
+ };
3577
+ destroyRef.onDestroy(() => stop());
3578
+ if (opts.watch)
3579
+ watch(opts);
3580
+ return {
3581
+ position: position.asReadonly(),
3582
+ error: error.asReadonly(),
3583
+ watching: watching.asReadonly(),
3584
+ isSupported: supported.asReadonly(),
3585
+ watch,
3586
+ stop,
3587
+ getCurrent,
3588
+ };
3589
+ }
3590
+
3591
+ function injectBattery() {
3592
+ const destroyRef = inject(DestroyRef);
3593
+ const platformId = inject(PLATFORM_ID);
3594
+ const isBrowser = isPlatformBrowser(platformId);
3595
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'getBattery' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3596
+ const info = signal(null, ...(ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []));
3597
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3598
+ let manager = null;
3599
+ let disposed = false;
3600
+ const snapshot = () => manager
3601
+ ? {
3602
+ charging: manager.charging,
3603
+ level: manager.level,
3604
+ chargingTime: manager.chargingTime,
3605
+ dischargingTime: manager.dischargingTime,
3606
+ }
3607
+ : null;
3608
+ const update = () => {
3609
+ if (!disposed)
3610
+ info.set(snapshot());
3611
+ };
3612
+ const events = [
3613
+ 'chargingchange',
3614
+ 'levelchange',
3615
+ 'chargingtimechange',
3616
+ 'dischargingtimechange',
3617
+ ];
3618
+ const refresh = async () => {
3619
+ if (!supported() || disposed)
3620
+ return;
3621
+ try {
3622
+ if (!manager) {
3623
+ const nav = navigator;
3624
+ manager = await nav.getBattery();
3625
+ for (const ev of events) {
3626
+ manager.addEventListener(ev, update);
3627
+ }
3628
+ }
3629
+ update();
3630
+ }
3631
+ catch (err) {
3632
+ if (!disposed)
3633
+ error.set(err instanceof Error ? err.message : 'getBattery failed');
3634
+ }
3635
+ };
3636
+ destroyRef.onDestroy(() => {
3637
+ disposed = true;
3638
+ if (manager) {
3639
+ for (const ev of events) {
3640
+ manager.removeEventListener(ev, update);
3641
+ }
3642
+ manager = null;
3643
+ }
3644
+ });
3645
+ if (supported())
3646
+ void refresh();
3647
+ return {
3648
+ info: info.asReadonly(),
3649
+ error: error.asReadonly(),
3650
+ isSupported: supported.asReadonly(),
3651
+ refresh,
3652
+ };
3653
+ }
3654
+
3655
+ function injectWakeLock() {
3656
+ const destroyRef = inject(DestroyRef);
3657
+ const platformId = inject(PLATFORM_ID);
3658
+ const isBrowser = isPlatformBrowser(platformId);
3659
+ const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'wakeLock' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
3660
+ const active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
3661
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3662
+ let sentinel = null;
3663
+ let disposed = false;
3664
+ const onRelease = () => {
3665
+ if (!disposed)
3666
+ active.set(false);
3667
+ sentinel = null;
3668
+ };
3669
+ const request = async () => {
3670
+ if (!supported() || disposed) {
3671
+ error.set('Screen Wake Lock API not supported');
3672
+ return false;
3673
+ }
3674
+ if (sentinel && !sentinel.released)
3675
+ return true;
3676
+ try {
3677
+ const nav = navigator;
3678
+ sentinel = await nav.wakeLock.request('screen');
3679
+ sentinel.addEventListener('release', onRelease);
3680
+ if (!disposed) {
3681
+ active.set(true);
3682
+ error.set(null);
3683
+ }
3684
+ return true;
3685
+ }
3686
+ catch (err) {
3687
+ if (!disposed)
3688
+ error.set(err instanceof Error ? err.message : 'wakeLock.request failed');
3689
+ return false;
3690
+ }
3691
+ };
3692
+ const release = async () => {
3693
+ if (!sentinel || sentinel.released)
3694
+ return;
3695
+ try {
3696
+ await sentinel.release();
3697
+ }
3698
+ finally {
3699
+ onRelease();
3700
+ }
3701
+ };
3702
+ destroyRef.onDestroy(() => {
3703
+ disposed = true;
3704
+ if (sentinel && !sentinel.released) {
3705
+ void sentinel.release();
3706
+ }
3707
+ sentinel = null;
3708
+ });
3709
+ return {
3710
+ active: active.asReadonly(),
3711
+ error: error.asReadonly(),
3712
+ isSupported: supported.asReadonly(),
3713
+ request,
3714
+ release,
3715
+ };
3716
+ }
3717
+
3166
3718
  class BrowserSupportUtil {
3167
3719
  static isSupported(feature) {
3168
3720
  if (typeof window === 'undefined' || typeof navigator === 'undefined') {
@@ -3389,6 +3941,18 @@ function provideGamepad() {
3389
3941
  return makeEnvironmentProviders([GamepadService]);
3390
3942
  }
3391
3943
 
3944
+ function provideWebLocks() {
3945
+ return makeEnvironmentProviders([WebLocksService]);
3946
+ }
3947
+
3948
+ function provideStorageManager() {
3949
+ return makeEnvironmentProviders([StorageManagerService]);
3950
+ }
3951
+
3952
+ function provideCompression() {
3953
+ return makeEnvironmentProviders([CompressionService]);
3954
+ }
3955
+
3392
3956
  function provideMediaApis() {
3393
3957
  return makeEnvironmentProviders([PermissionsService, CameraService, MediaDevicesService]);
3394
3958
  }
@@ -3484,4 +4048,4 @@ const version = '0.1.0';
3484
4048
  * Generated bundle index. Do not edit.
3485
4049
  */
3486
4050
 
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 };
4051
+ export { BROWSER_API_LOGGER, BatteryService, BroadcastChannelService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, CompressionService, ConnectionRegistryBaseService, FileSystemAccessService, FullscreenService, GamepadService, GeolocationService, IntersectionObserverService, MediaDevicesService, MediaRecorderService, MutationObserverService, NetworkInformationService, NotificationService, PageVisibilityService, PerformanceObserverService, PermissionsService, ResizeObserverService, ScreenOrientationService, ScreenWakeLockService, ServerSentEventsService, SpeechSynthesisService, StorageManagerService, VibrationService, WebAudioService, WebLocksService, 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, provideCompression, provideFileSystemAccess, provideFullscreen, provideGamepad, provideGeolocation, provideIntersectionObserver, provideLocationApis, provideMediaApis, provideMediaDevices, provideMediaRecorder, provideMutationObserver, provideNetworkInformation, provideNotifications, providePageVisibility, providePerformanceObserver, providePermissions, provideResizeObserver, provideScreenOrientation, provideScreenWakeLock, provideServerSentEvents, provideSpeechSynthesis, provideStorageApis, provideStorageManager, provideVibration, provideWebAudio, provideWebLocks, provideWebShare, provideWebSocket, provideWebStorage, provideWebWorker, version };