@angular-helpers/browser-web-apis 21.4.0 → 21.6.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,33 +1,22 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, DestroyRef, PLATFORM_ID, signal, computed, isSignal, effect, ElementRef, makeEnvironmentProviders } from '@angular/core';
2
+ import { InjectionToken, inject, DestroyRef, PLATFORM_ID, Injectable, 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 { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
6
6
  import { filter, distinctUntilChanged, map } from 'rxjs/operators';
7
7
  import { Router } from '@angular/router';
8
8
 
9
- class PermissionsService {
10
- async query(descriptor) {
11
- if (!this.isSupported()) {
12
- throw new Error('Permissions API not supported');
13
- }
14
- try {
15
- return await navigator.permissions.query(descriptor);
16
- }
17
- catch (error) {
18
- console.error('Error querying permission:', error);
19
- throw error;
20
- }
21
- }
22
- isSupported() {
23
- return typeof navigator !== 'undefined' && 'permissions' in navigator;
24
- }
25
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
26
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService });
27
- }
28
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, decorators: [{
29
- type: Injectable
30
- }] });
9
+ const BROWSER_API_LOGGER = new InjectionToken('BROWSER_API_LOGGER', {
10
+ providedIn: 'root',
11
+ factory: () => ({
12
+ // oxlint-disable-next-line no-console
13
+ info: (message) => console.info(message),
14
+ // oxlint-disable-next-line no-console
15
+ warn: (message) => console.warn(message),
16
+ // oxlint-disable-next-line no-console
17
+ error: (message, error) => console.error(message, error),
18
+ }),
19
+ });
31
20
 
32
21
  /**
33
22
  * Base class for all Browser Web API services.
@@ -35,7 +24,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
35
24
  * - Platform detection (browser vs server)
36
25
  * - Support assertion via Template Method
37
26
  * - Error creation with cause chaining
38
- * - Structured logging
27
+ * - Structured logging via injectable BROWSER_API_LOGGER token
39
28
  * - Lifecycle management with destroyRef
40
29
  *
41
30
  * Services that also need permission querying should extend
@@ -44,6 +33,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
44
33
  class BrowserApiBaseService {
45
34
  destroyRef = inject(DestroyRef);
46
35
  platformId = inject(PLATFORM_ID);
36
+ logger = inject(BROWSER_API_LOGGER);
47
37
  /**
48
38
  * Check if running in browser environment using Angular's platform detection.
49
39
  */
@@ -57,8 +47,8 @@ class BrowserApiBaseService {
57
47
  return isPlatformServer(this.platformId);
58
48
  }
59
49
  /**
60
- * Template Method: throws a standard error when the API is not available.
61
- * Uses getApiName() to produce consistent error messages.
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.
62
52
  */
63
53
  ensureSupported() {
64
54
  if (!this.isBrowserEnvironment()) {
@@ -76,16 +66,22 @@ class BrowserApiBaseService {
76
66
  return error;
77
67
  }
78
68
  /**
79
- * Log an error with the service name as prefix.
69
+ * Log an error through the injected BROWSER_API_LOGGER (default: console).
80
70
  */
81
71
  logError(message, error) {
82
- console.error(`[${this.getApiName()}] ${message}`, error);
72
+ this.logger.error(`[${this.getApiName()}] ${message}`, error);
73
+ }
74
+ /**
75
+ * Log a warning through the injected BROWSER_API_LOGGER (default: console).
76
+ */
77
+ logWarn(message) {
78
+ this.logger.warn(`[${this.getApiName()}] ${message}`);
83
79
  }
84
80
  /**
85
- * Log an informational message with the service name as prefix.
81
+ * Log an informational message through the injected BROWSER_API_LOGGER (default: console).
86
82
  */
87
83
  logInfo(message) {
88
- console.info(`[${this.getApiName()}] ${message}`);
84
+ this.logger.info(`[${this.getApiName()}] ${message}`);
89
85
  }
90
86
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserApiBaseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
91
87
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserApiBaseService });
@@ -94,59 +90,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
94
90
  type: Injectable
95
91
  }] });
96
92
 
97
- /**
98
- * Extension of `BrowserApiBaseService` for services that need to query
99
- * browser permissions via the Permissions API.
100
- *
101
- * Only services that actively call `requestPermission()` should extend this
102
- * class. All other services should extend `BrowserApiBaseService` directly
103
- * to avoid carrying an unnecessary PermissionsService dependency.
104
- */
105
- class PermissionAwareBrowserApiBaseService extends BrowserApiBaseService {
106
- permissionsService = inject(PermissionsService);
107
- /**
108
- * Query the Permissions API for the given permission name.
109
- * Returns `true` if the permission state is `'granted'`.
110
- */
111
- async requestPermission(permission) {
112
- if (this.isServerEnvironment()) {
113
- throw new Error(`${this.getApiName()} API not available in server environment`);
93
+ class PermissionsService extends BrowserApiBaseService {
94
+ getApiName() {
95
+ return 'permissions';
96
+ }
97
+ async query(descriptor) {
98
+ if (!this.isSupported()) {
99
+ throw new Error('Permissions API not supported in this environment');
114
100
  }
115
101
  try {
116
- const status = await this.permissionsService.query({ name: permission });
117
- return status.state === 'granted';
102
+ return await navigator.permissions.query(descriptor);
118
103
  }
119
104
  catch (error) {
120
- this.logError(`Error requesting permission for ${permission}:`, error);
121
- return false;
105
+ // Firefox does not support querying 'camera', 'microphone', or 'speaker' via
106
+ // the Permissions API and throws a TypeError. Return a synthetic 'prompt' status
107
+ // so callers fall through to the native getUserMedia / requestPermission flow.
108
+ if (error instanceof TypeError) {
109
+ return { state: 'prompt', onchange: null };
110
+ }
111
+ this.logError(`Error querying permission for ${descriptor.name}:`, error);
112
+ throw error;
122
113
  }
123
114
  }
124
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionAwareBrowserApiBaseService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
125
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionAwareBrowserApiBaseService });
115
+ isSupported() {
116
+ return this.isBrowserEnvironment() && 'permissions' in navigator;
117
+ }
118
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
119
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService });
126
120
  }
127
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionAwareBrowserApiBaseService, decorators: [{
121
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, decorators: [{
128
122
  type: Injectable
129
123
  }] });
130
124
 
131
- class CameraService extends PermissionAwareBrowserApiBaseService {
125
+ class CameraService extends BrowserApiBaseService {
132
126
  currentStream = null;
133
127
  getApiName() {
134
128
  return 'camera';
135
129
  }
136
- ensureCameraSupport() {
137
- if (!('mediaDevices' in navigator) || !('getUserMedia' in navigator.mediaDevices)) {
138
- throw new Error('Camera API not supported in this browser');
130
+ ensureSupported() {
131
+ super.ensureSupported();
132
+ if (!navigator.mediaDevices?.getUserMedia) {
133
+ throw new Error('Camera API not supported — a secure context (HTTPS) is required');
139
134
  }
140
135
  }
141
136
  async startCamera(constraints) {
142
- this.ensureCameraSupport();
137
+ this.ensureSupported();
143
138
  if (this.currentStream) {
144
139
  this.stopCamera();
145
140
  }
146
- const permissionStatus = await this.permissionsService.query({ name: 'camera' });
147
- if (permissionStatus.state !== 'granted') {
148
- throw new Error('Camera permission required. Please grant camera access and try again.');
149
- }
150
141
  try {
151
142
  const streamConstraints = constraints || {
152
143
  video: {
@@ -159,7 +150,7 @@ class CameraService extends PermissionAwareBrowserApiBaseService {
159
150
  return this.currentStream;
160
151
  }
161
152
  catch (error) {
162
- console.error('[CameraService] Error starting camera:', error);
153
+ this.logError('Error starting camera:', error);
163
154
  if (error instanceof Error && error.name === 'NotAllowedError') {
164
155
  throw this.createError('Camera permission denied by user. Please allow camera access in your browser settings and refresh the page.', error);
165
156
  }
@@ -201,19 +192,24 @@ class CameraService extends PermissionAwareBrowserApiBaseService {
201
192
  return this.startCamera(finalConstraints);
202
193
  }
203
194
  async getCameraCapabilities(deviceId) {
204
- this.ensureCameraSupport();
195
+ this.ensureSupported();
205
196
  try {
197
+ const activeTrack = this.currentStream
198
+ ?.getVideoTracks()
199
+ .find((t) => t.getSettings().deviceId === deviceId);
200
+ if (activeTrack) {
201
+ return activeTrack.getCapabilities() ?? null;
202
+ }
206
203
  const stream = await navigator.mediaDevices.getUserMedia({
207
204
  video: { deviceId: { exact: deviceId } },
208
205
  });
209
206
  const videoTrack = stream.getVideoTracks()[0];
210
207
  const capabilities = videoTrack.getCapabilities();
211
- // Clean up the stream
212
208
  stream.getTracks().forEach((track) => track.stop());
213
- return capabilities || null;
209
+ return capabilities ?? null;
214
210
  }
215
211
  catch (error) {
216
- console.error('[CameraService] Error getting camera capabilities:', error);
212
+ this.logError('Error getting camera capabilities:', error);
217
213
  return null;
218
214
  }
219
215
  }
@@ -224,19 +220,19 @@ class CameraService extends PermissionAwareBrowserApiBaseService {
224
220
  return this.currentStream !== null;
225
221
  }
226
222
  async getVideoInputDevices() {
227
- this.ensureCameraSupport();
223
+ this.ensureSupported();
228
224
  try {
229
225
  const devices = await navigator.mediaDevices.enumerateDevices();
230
226
  return devices.filter((device) => device.kind === 'videoinput');
231
227
  }
232
228
  catch (error) {
233
- console.error('[CameraService] Error enumerating video devices:', error);
229
+ this.logError('Error enumerating video devices:', error);
234
230
  throw this.createError('Failed to enumerate video devices', error);
235
231
  }
236
232
  }
237
233
  // Direct access to native camera API
238
234
  getNativeMediaDevices() {
239
- this.ensureCameraSupport();
235
+ this.ensureSupported();
240
236
  return navigator.mediaDevices;
241
237
  }
242
238
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CameraService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
@@ -250,13 +246,14 @@ class GeolocationService extends BrowserApiBaseService {
250
246
  getApiName() {
251
247
  return 'geolocation';
252
248
  }
253
- ensureGeolocationSupport() {
249
+ ensureSupported() {
250
+ super.ensureSupported();
254
251
  if (!('geolocation' in navigator)) {
255
- throw new Error('Geolocation API not supported in this browser');
252
+ throw new Error('Geolocation API not supported a secure context (HTTPS) is required');
256
253
  }
257
254
  }
258
255
  getCurrentPosition(options) {
259
- this.ensureGeolocationSupport();
256
+ this.ensureSupported();
260
257
  return new Promise((resolve, reject) => {
261
258
  navigator.geolocation.getCurrentPosition((position) => resolve(position), (error) => {
262
259
  this.logError('Error getting position:', error);
@@ -265,7 +262,7 @@ class GeolocationService extends BrowserApiBaseService {
265
262
  });
266
263
  }
267
264
  watchPosition(options) {
268
- this.ensureGeolocationSupport();
265
+ this.ensureSupported();
269
266
  return new Observable((observer) => {
270
267
  const watchId = navigator.geolocation.watchPosition((position) => observer.next(position), (error) => {
271
268
  this.logError('Error watching position:', error);
@@ -281,7 +278,7 @@ class GeolocationService extends BrowserApiBaseService {
281
278
  }
282
279
  // Direct access to native geolocation API
283
280
  getNativeGeolocation() {
284
- this.ensureGeolocationSupport();
281
+ this.ensureSupported();
285
282
  return navigator.geolocation;
286
283
  }
287
284
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: GeolocationService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
@@ -295,24 +292,25 @@ class MediaDevicesService extends BrowserApiBaseService {
295
292
  getApiName() {
296
293
  return 'media-devices';
297
294
  }
298
- ensureMediaDevicesSupport() {
299
- if (!('mediaDevices' in navigator)) {
300
- throw new Error('MediaDevices API not supported in this browser');
295
+ ensureSupported() {
296
+ super.ensureSupported();
297
+ if (!navigator.mediaDevices) {
298
+ throw new Error('MediaDevices API not supported — a secure context (HTTPS) is required');
301
299
  }
302
300
  }
303
301
  async getDevices() {
304
- this.ensureMediaDevicesSupport();
302
+ this.ensureSupported();
305
303
  try {
306
304
  const devices = await navigator.mediaDevices.enumerateDevices();
307
305
  return devices;
308
306
  }
309
307
  catch (error) {
310
- console.error('[MediaDevicesService] Error enumerating devices:', error);
308
+ this.logError('Error enumerating devices:', error);
311
309
  throw this.createError('Failed to enumerate media devices', error);
312
310
  }
313
311
  }
314
312
  async getUserMedia(constraints) {
315
- this.ensureMediaDevicesSupport();
313
+ this.ensureSupported();
316
314
  try {
317
315
  const defaultConstraints = {
318
316
  video: true,
@@ -322,12 +320,12 @@ class MediaDevicesService extends BrowserApiBaseService {
322
320
  return await navigator.mediaDevices.getUserMedia(finalConstraints);
323
321
  }
324
322
  catch (error) {
325
- console.error('[MediaDevicesService] Error getting user media:', error);
323
+ this.logError('Error getting user media:', error);
326
324
  throw this.handleMediaError(error);
327
325
  }
328
326
  }
329
327
  async getDisplayMedia(constraints) {
330
- this.ensureMediaDevicesSupport();
328
+ this.ensureSupported();
331
329
  if (!('getDisplayMedia' in navigator.mediaDevices)) {
332
330
  throw new Error('Display media API not supported in this browser');
333
331
  }
@@ -340,12 +338,12 @@ class MediaDevicesService extends BrowserApiBaseService {
340
338
  return await navigator.mediaDevices.getDisplayMedia(finalConstraints);
341
339
  }
342
340
  catch (error) {
343
- console.error('[MediaDevicesService] Error getting display media:', error);
341
+ this.logError('Error getting display media:', error);
344
342
  throw this.createError('Failed to get display media', error);
345
343
  }
346
344
  }
347
345
  watchDeviceChanges() {
348
- this.ensureMediaDevicesSupport();
346
+ this.ensureSupported();
349
347
  return new Observable((observer) => {
350
348
  const handleDeviceChange = async () => {
351
349
  try {
@@ -353,7 +351,7 @@ class MediaDevicesService extends BrowserApiBaseService {
353
351
  observer.next(devices);
354
352
  }
355
353
  catch (error) {
356
- console.error('[MediaDevicesService] Error handling device change:', error);
354
+ this.logError('Error handling device change:', error);
357
355
  }
358
356
  };
359
357
  // Listen for device changes
@@ -408,7 +406,7 @@ class MediaDevicesService extends BrowserApiBaseService {
408
406
  }
409
407
  // Direct access to native media devices API
410
408
  getNativeMediaDevices() {
411
- this.ensureMediaDevicesSupport();
409
+ this.ensureSupported();
412
410
  return navigator.mediaDevices;
413
411
  }
414
412
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: MediaDevicesService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
@@ -422,22 +420,24 @@ class NotificationService extends BrowserApiBaseService {
422
420
  getApiName() {
423
421
  return 'notifications';
424
422
  }
423
+ ensureSupported() {
424
+ super.ensureSupported();
425
+ if (!('Notification' in window)) {
426
+ throw new Error('Notifications API not supported in this browser');
427
+ }
428
+ }
425
429
  get permission() {
430
+ if (!this.isBrowserEnvironment() || !('Notification' in window)) {
431
+ return 'default';
432
+ }
426
433
  return Notification.permission;
427
434
  }
428
- isSupported() {
429
- return this.isBrowserEnvironment() && 'Notification' in window;
430
- }
431
435
  async requestNotificationPermission() {
432
- if (!this.isSupported()) {
433
- throw new Error('Notification API not supported in this browser');
434
- }
436
+ this.ensureSupported();
435
437
  return Notification.requestPermission();
436
438
  }
437
439
  async showNotification(title, options) {
438
- if (!this.isSupported()) {
439
- throw new Error('Notification API not supported in this browser');
440
- }
440
+ this.ensureSupported();
441
441
  if (Notification.permission !== 'granted') {
442
442
  throw new Error('Notification permission required. Please grant notification access and try again.');
443
443
  }
@@ -456,38 +456,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
456
456
  type: Injectable
457
457
  }] });
458
458
 
459
- class ClipboardService extends PermissionAwareBrowserApiBaseService {
459
+ class ClipboardService extends BrowserApiBaseService {
460
460
  getApiName() {
461
461
  return 'clipboard';
462
462
  }
463
- async ensureClipboardPermission(action) {
464
- if (!('clipboard' in navigator)) {
465
- throw new Error('Clipboard API not supported in this browser');
466
- }
467
- const permissionStatus = await this.permissionsService.query({
468
- name: `clipboard-${action}`,
469
- });
470
- if (permissionStatus.state !== 'granted') {
471
- throw new Error(`Clipboard ${action} permission required. Please grant clipboard access and try again.`);
463
+ ensureSupported() {
464
+ super.ensureSupported();
465
+ if (!navigator.clipboard) {
466
+ throw new Error('Clipboard API not supported \u2014 a secure context (HTTPS) is required');
472
467
  }
473
468
  }
474
469
  async writeText(text) {
475
- await this.ensureClipboardPermission('write');
470
+ this.ensureSupported();
476
471
  try {
477
472
  await navigator.clipboard.writeText(text);
478
473
  }
479
474
  catch (error) {
480
- console.error('[ClipboardService] Error writing to clipboard:', error);
475
+ this.logError('Error writing to clipboard:', error);
481
476
  throw error;
482
477
  }
483
478
  }
484
479
  async readText() {
485
- await this.ensureClipboardPermission('read');
480
+ this.ensureSupported();
486
481
  try {
487
482
  return await navigator.clipboard.readText();
488
483
  }
489
484
  catch (error) {
490
- console.error('[ClipboardService] Error reading from clipboard:', error);
485
+ this.logError('Error reading from clipboard:', error);
491
486
  throw error;
492
487
  }
493
488
  }
@@ -659,14 +654,15 @@ class BatteryService extends BrowserApiBaseService {
659
654
  getApiName() {
660
655
  return 'battery';
661
656
  }
662
- ensureBatterySupport() {
657
+ ensureSupported() {
658
+ super.ensureSupported();
663
659
  const nav = navigator;
664
660
  if (!('getBattery' in nav)) {
665
- throw new Error('Battery API not supported in this browser');
661
+ throw new Error('Battery Status API not supported in this browser');
666
662
  }
667
663
  }
668
664
  async initialize() {
669
- this.ensureBatterySupport();
665
+ this.ensureSupported();
670
666
  try {
671
667
  const nav = navigator;
672
668
  this.batteryManager = await nav.getBattery();
@@ -674,7 +670,7 @@ class BatteryService extends BrowserApiBaseService {
674
670
  return batteryInfo;
675
671
  }
676
672
  catch (error) {
677
- console.error('[BatteryService] Error initializing battery API:', error);
673
+ this.logError('Error initializing battery API:', error);
678
674
  throw this.createError('Failed to initialize battery API', error);
679
675
  }
680
676
  }
@@ -743,19 +739,20 @@ class WebShareService extends BrowserApiBaseService {
743
739
  getApiName() {
744
740
  return 'web-share';
745
741
  }
746
- ensureWebShareSupport() {
742
+ ensureSupported() {
743
+ super.ensureSupported();
747
744
  if (!('share' in navigator)) {
748
745
  throw new Error('Web Share API not supported in this browser');
749
746
  }
750
747
  }
751
748
  async share(data) {
752
- this.ensureWebShareSupport();
749
+ this.ensureSupported();
753
750
  try {
754
751
  await navigator.share(data);
755
752
  return { shared: true };
756
753
  }
757
754
  catch (error) {
758
- console.error('[WebShareService] Error sharing:', error);
755
+ this.logError('Error sharing:', error);
759
756
  const errorMessage = error instanceof Error ? error.message : 'Share failed';
760
757
  return { shared: false, error: errorMessage };
761
758
  }
@@ -772,7 +769,7 @@ class WebShareService extends BrowserApiBaseService {
772
769
  }
773
770
  // Direct access to native share API
774
771
  getNativeShare() {
775
- this.ensureWebShareSupport();
772
+ this.ensureSupported();
776
773
  return navigator.share;
777
774
  }
778
775
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebShareService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
@@ -784,7 +781,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
784
781
 
785
782
  class WebStorageService extends BrowserApiBaseService {
786
783
  storageEvents = signal(null, ...(ngDevMode ? [{ debugName: "storageEvents" }] : /* istanbul ignore next */ []));
787
- destroyRef = inject(DestroyRef);
788
784
  constructor() {
789
785
  super();
790
786
  this.setupEventListeners();
@@ -792,8 +788,9 @@ class WebStorageService extends BrowserApiBaseService {
792
788
  getApiName() {
793
789
  return 'storage';
794
790
  }
795
- ensureStorageSupport() {
796
- if (!this.isBrowserEnvironment() || typeof Storage === 'undefined') {
791
+ ensureSupported() {
792
+ super.ensureSupported();
793
+ if (typeof Storage === 'undefined') {
797
794
  throw new Error('Storage API not supported in this browser');
798
795
  }
799
796
  }
@@ -803,14 +800,13 @@ class WebStorageService extends BrowserApiBaseService {
803
800
  .pipe(takeUntilDestroyed(this.destroyRef))
804
801
  .subscribe((event) => {
805
802
  const storageEvent = event;
806
- if (storageEvent.key && storageEvent.newValue !== null) {
807
- this.storageEvents.set({
808
- key: storageEvent.key,
809
- newValue: this.deserializeValue(storageEvent.newValue),
810
- oldValue: storageEvent.oldValue ? this.deserializeValue(storageEvent.oldValue) : null,
811
- storageArea: storageEvent.storageArea === localStorage ? 'localStorage' : 'sessionStorage',
812
- });
813
- }
803
+ const area = storageEvent.storageArea === localStorage ? 'localStorage' : 'sessionStorage';
804
+ this.storageEvents.set({
805
+ key: storageEvent.key,
806
+ newValue: storageEvent.newValue ? this.deserializeValue(storageEvent.newValue) : null,
807
+ oldValue: storageEvent.oldValue ? this.deserializeValue(storageEvent.oldValue) : null,
808
+ storageArea: area,
809
+ });
814
810
  });
815
811
  }
816
812
  }
@@ -837,50 +833,58 @@ class WebStorageService extends BrowserApiBaseService {
837
833
  const prefix = options?.prefix || '';
838
834
  return prefix ? `${prefix}:${key}` : key;
839
835
  }
836
+ emitStorageChange(fullKey, newValue, oldValue, area) {
837
+ this.storageEvents.set({ key: fullKey, newValue, oldValue, storageArea: area });
838
+ }
840
839
  // Local Storage Methods
841
840
  setLocalStorage(key, value, options = {}) {
842
- this.ensureStorageSupport();
841
+ this.ensureSupported();
843
842
  try {
844
843
  const serializedValue = this.serializeValue(value, options);
845
844
  const fullKey = this.getKey(key, options);
845
+ const oldRaw = localStorage.getItem(fullKey);
846
+ const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
846
847
  localStorage.setItem(fullKey, serializedValue);
848
+ this.emitStorageChange(fullKey, value, oldValue, 'localStorage');
847
849
  return true;
848
850
  }
849
851
  catch (error) {
850
- console.error('[WebStorageService] Error setting localStorage:', error);
852
+ this.logError('Error setting localStorage:', error);
851
853
  return false;
852
854
  }
853
855
  }
854
856
  getLocalStorage(key, defaultValue = null, options = {}) {
855
- this.ensureStorageSupport();
857
+ this.ensureSupported();
856
858
  try {
857
859
  const fullKey = this.getKey(key, options);
858
860
  const value = localStorage.getItem(fullKey);
859
861
  return value !== null ? this.deserializeValue(value, options) : defaultValue;
860
862
  }
861
863
  catch (error) {
862
- console.error('[WebStorageService] Error getting localStorage:', error);
864
+ this.logError('Error getting localStorage:', error);
863
865
  return defaultValue;
864
866
  }
865
867
  }
866
868
  removeLocalStorage(key, options = {}) {
867
- this.ensureStorageSupport();
869
+ this.ensureSupported();
868
870
  try {
869
871
  const fullKey = this.getKey(key, options);
872
+ const oldRaw = localStorage.getItem(fullKey);
873
+ const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
870
874
  localStorage.removeItem(fullKey);
875
+ this.emitStorageChange(fullKey, null, oldValue, 'localStorage');
871
876
  return true;
872
877
  }
873
878
  catch (error) {
874
- console.error('[WebStorageService] Error removing localStorage:', error);
879
+ this.logError('Error removing localStorage:', error);
875
880
  return false;
876
881
  }
877
882
  }
878
883
  clearLocalStorage(options = {}) {
879
- this.ensureStorageSupport();
884
+ this.ensureSupported();
880
885
  try {
881
886
  const prefix = options?.prefix;
882
887
  if (prefix) {
883
- // Only remove keys with the specified prefix
884
888
  const keysToRemove = [];
885
889
  for (let i = 0; i < localStorage.length; i++) {
886
890
  const key = localStorage.key(i);
@@ -888,62 +892,72 @@ class WebStorageService extends BrowserApiBaseService {
888
892
  keysToRemove.push(key);
889
893
  }
890
894
  }
891
- keysToRemove.forEach((key) => localStorage.removeItem(key));
895
+ keysToRemove.forEach((key) => {
896
+ const oldRaw = localStorage.getItem(key);
897
+ localStorage.removeItem(key);
898
+ this.emitStorageChange(key, null, oldRaw, 'localStorage');
899
+ });
892
900
  }
893
901
  else {
894
902
  localStorage.clear();
903
+ this.emitStorageChange(null, null, null, 'localStorage');
895
904
  }
896
905
  return true;
897
906
  }
898
907
  catch (error) {
899
- console.error('[WebStorageService] Error clearing localStorage:', error);
908
+ this.logError('Error clearing localStorage:', error);
900
909
  return false;
901
910
  }
902
911
  }
903
912
  // Session Storage Methods
904
913
  setSessionStorage(key, value, options = {}) {
905
- this.ensureStorageSupport();
914
+ this.ensureSupported();
906
915
  try {
907
916
  const serializedValue = this.serializeValue(value, options);
908
917
  const fullKey = this.getKey(key, options);
918
+ const oldRaw = sessionStorage.getItem(fullKey);
919
+ const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
909
920
  sessionStorage.setItem(fullKey, serializedValue);
921
+ this.emitStorageChange(fullKey, value, oldValue, 'sessionStorage');
910
922
  return true;
911
923
  }
912
924
  catch (error) {
913
- console.error('[WebStorageService] Error setting sessionStorage:', error);
925
+ this.logError('Error setting sessionStorage:', error);
914
926
  return false;
915
927
  }
916
928
  }
917
929
  getSessionStorage(key, defaultValue = null, options = {}) {
918
- this.ensureStorageSupport();
930
+ this.ensureSupported();
919
931
  try {
920
932
  const fullKey = this.getKey(key, options);
921
933
  const value = sessionStorage.getItem(fullKey);
922
934
  return value !== null ? this.deserializeValue(value, options) : defaultValue;
923
935
  }
924
936
  catch (error) {
925
- console.error('[WebStorageService] Error getting sessionStorage:', error);
937
+ this.logError('Error getting sessionStorage:', error);
926
938
  return defaultValue;
927
939
  }
928
940
  }
929
941
  removeSessionStorage(key, options = {}) {
930
- this.ensureStorageSupport();
942
+ this.ensureSupported();
931
943
  try {
932
944
  const fullKey = this.getKey(key, options);
945
+ const oldRaw = sessionStorage.getItem(fullKey);
946
+ const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
933
947
  sessionStorage.removeItem(fullKey);
948
+ this.emitStorageChange(fullKey, null, oldValue, 'sessionStorage');
934
949
  return true;
935
950
  }
936
951
  catch (error) {
937
- console.error('[WebStorageService] Error removing sessionStorage:', error);
952
+ this.logError('Error removing sessionStorage:', error);
938
953
  return false;
939
954
  }
940
955
  }
941
956
  clearSessionStorage(options = {}) {
942
- this.ensureStorageSupport();
957
+ this.ensureSupported();
943
958
  try {
944
959
  const prefix = options?.prefix;
945
960
  if (prefix) {
946
- // Only remove keys with the specified prefix
947
961
  const keysToRemove = [];
948
962
  for (let i = 0; i < sessionStorage.length; i++) {
949
963
  const key = sessionStorage.key(i);
@@ -951,21 +965,26 @@ class WebStorageService extends BrowserApiBaseService {
951
965
  keysToRemove.push(key);
952
966
  }
953
967
  }
954
- keysToRemove.forEach((key) => sessionStorage.removeItem(key));
968
+ keysToRemove.forEach((key) => {
969
+ const oldRaw = sessionStorage.getItem(key);
970
+ sessionStorage.removeItem(key);
971
+ this.emitStorageChange(key, null, oldRaw, 'sessionStorage');
972
+ });
955
973
  }
956
974
  else {
957
975
  sessionStorage.clear();
976
+ this.emitStorageChange(null, null, null, 'sessionStorage');
958
977
  }
959
978
  return true;
960
979
  }
961
980
  catch (error) {
962
- console.error('[WebStorageService] Error clearing sessionStorage:', error);
981
+ this.logError('Error clearing sessionStorage:', error);
963
982
  return false;
964
983
  }
965
984
  }
966
985
  // Utility Methods
967
986
  getLocalStorageSize(options = {}) {
968
- this.ensureStorageSupport();
987
+ this.ensureSupported();
969
988
  let totalSize = 0;
970
989
  const prefix = options?.prefix;
971
990
  for (let i = 0; i < localStorage.length; i++) {
@@ -977,7 +996,7 @@ class WebStorageService extends BrowserApiBaseService {
977
996
  return totalSize;
978
997
  }
979
998
  getSessionStorageSize(options = {}) {
980
- this.ensureStorageSupport();
999
+ this.ensureSupported();
981
1000
  let totalSize = 0;
982
1001
  const prefix = options?.prefix;
983
1002
  for (let i = 0; i < sessionStorage.length; i++) {
@@ -994,30 +1013,20 @@ class WebStorageService extends BrowserApiBaseService {
994
1013
  prev.oldValue === curr.oldValue));
995
1014
  }
996
1015
  watchLocalStorage(key, options = {}) {
997
- return this.getStorageEvents().pipe(map((event) => {
998
- const fullKey = this.getKey(key, options);
999
- if (event.key === fullKey && event.storageArea === 'localStorage') {
1000
- return event.newValue;
1001
- }
1002
- return this.getLocalStorage(key, null, options);
1003
- }));
1016
+ const fullKey = this.getKey(key, options);
1017
+ return this.getStorageEvents().pipe(filter((event) => event.storageArea === 'localStorage' && (event.key === null || event.key === fullKey)), map((event) => event.newValue));
1004
1018
  }
1005
1019
  watchSessionStorage(key, options = {}) {
1006
- return this.getStorageEvents().pipe(map((event) => {
1007
- const fullKey = this.getKey(key, options);
1008
- if (event.key === fullKey && event.storageArea === 'sessionStorage') {
1009
- return event.newValue;
1010
- }
1011
- return this.getSessionStorage(key, null, options);
1012
- }));
1020
+ const fullKey = this.getKey(key, options);
1021
+ return this.getStorageEvents().pipe(filter((event) => event.storageArea === 'sessionStorage' && (event.key === null || event.key === fullKey)), map((event) => event.newValue));
1013
1022
  }
1014
1023
  // Direct access to native storage APIs
1015
1024
  getNativeLocalStorage() {
1016
- this.ensureStorageSupport();
1025
+ this.ensureSupported();
1017
1026
  return localStorage;
1018
1027
  }
1019
1028
  getNativeSessionStorage() {
1020
- this.ensureStorageSupport();
1029
+ this.ensureSupported();
1021
1030
  return sessionStorage;
1022
1031
  }
1023
1032
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -1027,229 +1036,480 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
1027
1036
  type: Injectable
1028
1037
  }], ctorParameters: () => [] });
1029
1038
 
1030
- class WebSocketService extends BrowserApiBaseService {
1031
- webSocket = null;
1032
- statusSubject = new Subject();
1033
- messageSubject = new Subject();
1034
- reconnectAttempts = 0;
1039
+ const DEFAULT_MAX_RECONNECT_DELAY = 30_000;
1040
+ const DEFAULT_REQUEST_TIMEOUT = 30_000;
1041
+ /**
1042
+ * Stateful WebSocket client wrapping a single connection. One instance per logical
1043
+ * connection (do NOT share between `connect()` calls).
1044
+ *
1045
+ * Surfaces:
1046
+ * - `status`: signal of the current connection state.
1047
+ * - `messages$`: stream of every received message (parsed JSON).
1048
+ * - `send` / `sendRaw`: outbound traffic.
1049
+ * - `request<T>(type, data)`: round-trip with id correlation and timeout.
1050
+ * - `close`: idempotent disposal.
1051
+ *
1052
+ * Reconnect uses exponential backoff with jitter, capped by `maxReconnectDelay`.
1053
+ */
1054
+ class WebSocketClient {
1055
+ config;
1056
+ logger;
1057
+ socket = null;
1035
1058
  reconnectTimer = null;
1036
1059
  heartbeatTimer = null;
1037
- getApiName() {
1038
- return 'websocket';
1039
- }
1040
- ensureWebSocketSupport() {
1041
- if (typeof WebSocket === 'undefined') {
1042
- throw new Error('WebSocket API not supported in this browser');
1060
+ _status;
1061
+ _messages$ = new Subject();
1062
+ pendingRequests = new Map();
1063
+ disposed = false;
1064
+ reconnectAttempts = 0;
1065
+ constructor(config, logger, destroyRef) {
1066
+ this.config = config;
1067
+ this.logger = logger;
1068
+ this._status = signal({
1069
+ state: 'idle',
1070
+ reconnectAttempts: 0,
1071
+ error: null,
1072
+ }, ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
1073
+ if (destroyRef) {
1074
+ destroyRef.onDestroy(() => this.close());
1043
1075
  }
1076
+ this.openSocket();
1044
1077
  }
1045
- connect(config) {
1046
- this.ensureWebSocketSupport();
1047
- return new Observable((observer) => {
1048
- this.disconnect(); // Disconnect existing connection if any
1049
- this.updateStatus({
1050
- connected: false,
1051
- connecting: true,
1052
- reconnecting: false,
1053
- reconnectAttempts: 0,
1054
- });
1055
- try {
1056
- this.webSocket = new WebSocket(config.url, config.protocols);
1057
- this.setupWebSocketHandlers(config);
1058
- observer.next(this.getCurrentStatus());
1059
- }
1060
- catch (error) {
1061
- console.error('[WebSocketService] Error creating WebSocket:', error);
1062
- this.updateStatus({
1063
- connected: false,
1064
- connecting: false,
1065
- reconnecting: false,
1066
- error: error instanceof Error ? error.message : 'Connection failed',
1067
- reconnectAttempts: 0,
1068
- });
1069
- observer.next(this.getCurrentStatus());
1070
- }
1071
- return () => {
1072
- this.disconnect();
1073
- };
1074
- });
1078
+ get status() {
1079
+ return this._status.asReadonly();
1075
1080
  }
1076
- disconnect() {
1077
- if (this.reconnectTimer) {
1078
- clearTimeout(this.reconnectTimer);
1079
- this.reconnectTimer = null;
1080
- }
1081
- if (this.heartbeatTimer) {
1082
- clearInterval(this.heartbeatTimer);
1083
- this.heartbeatTimer = null;
1084
- }
1085
- if (this.webSocket) {
1086
- this.webSocket.close();
1087
- this.webSocket = null;
1088
- }
1089
- this.updateStatus({
1090
- connected: false,
1091
- connecting: false,
1092
- reconnecting: false,
1093
- reconnectAttempts: 0,
1094
- });
1081
+ get messages$() {
1082
+ return this._messages$.asObservable();
1083
+ }
1084
+ messagesByType(type) {
1085
+ return this._messages$
1086
+ .asObservable()
1087
+ .pipe(filter((msg) => msg.type === type));
1095
1088
  }
1096
1089
  send(message) {
1097
- if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) {
1090
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
1098
1091
  throw new Error('WebSocket is not connected');
1099
1092
  }
1100
- const messageWithTimestamp = {
1093
+ const enriched = {
1101
1094
  ...message,
1102
- timestamp: Date.now(),
1095
+ id: message.id ?? this.generateId(),
1096
+ timestamp: message.timestamp ?? Date.now(),
1103
1097
  };
1104
- this.webSocket.send(JSON.stringify(messageWithTimestamp));
1098
+ this.socket.send(JSON.stringify(enriched));
1105
1099
  }
1106
1100
  sendRaw(data) {
1107
- if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) {
1101
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
1108
1102
  throw new Error('WebSocket is not connected');
1109
1103
  }
1110
- this.webSocket.send(data);
1104
+ this.socket.send(data);
1111
1105
  }
1112
- getStatus() {
1113
- return this.statusSubject.asObservable();
1106
+ /**
1107
+ * Send a message and await a correlated response. The server MUST echo back the
1108
+ * `correlationId` from the request as `correlationId` on the response message.
1109
+ */
1110
+ request(type, data, opts) {
1111
+ const id = this.generateId();
1112
+ const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
1113
+ return new Promise((resolve, reject) => {
1114
+ const timer = setTimeout(() => {
1115
+ this.pendingRequests.delete(id);
1116
+ reject(new Error(`WebSocket request timeout after ${timeoutMs}ms`));
1117
+ }, timeoutMs);
1118
+ this.pendingRequests.set(id, {
1119
+ resolve: resolve,
1120
+ reject,
1121
+ timer,
1122
+ });
1123
+ try {
1124
+ this.send({ id, type, data, correlationId: id });
1125
+ }
1126
+ catch (error) {
1127
+ clearTimeout(timer);
1128
+ this.pendingRequests.delete(id);
1129
+ reject(error instanceof Error ? error : new Error('WebSocket send failed'));
1130
+ }
1131
+ });
1114
1132
  }
1115
- getMessages() {
1116
- return this.messageSubject
1117
- .asObservable()
1118
- .pipe(filter((msg) => true));
1133
+ close() {
1134
+ if (this.disposed)
1135
+ return;
1136
+ this.disposed = true;
1137
+ this.clearTimers();
1138
+ if (this.socket) {
1139
+ try {
1140
+ this.socket.close();
1141
+ }
1142
+ catch {
1143
+ // Ignore — already closing.
1144
+ }
1145
+ this.socket = null;
1146
+ }
1147
+ this.rejectAllPending(new Error('WebSocket closed'));
1148
+ this.updateStatus({ state: 'closed', error: null });
1149
+ }
1150
+ /** Internal handle for tests and advanced usage. */
1151
+ getNativeSocket() {
1152
+ return this.socket;
1153
+ }
1154
+ // ---------- internals ----------
1155
+ openSocket() {
1156
+ if (this.disposed)
1157
+ return;
1158
+ this.updateStatus({ state: 'connecting', error: null });
1159
+ try {
1160
+ this.socket = new WebSocket(this.config.url, this.config.protocols);
1161
+ this.attachHandlers();
1162
+ }
1163
+ catch (error) {
1164
+ const message = error instanceof Error ? error.message : 'WebSocket open failed';
1165
+ this.logger.error('[websocket] Failed to construct socket', error);
1166
+ this.updateStatus({ state: 'closed', error: message });
1167
+ this.scheduleReconnect();
1168
+ }
1119
1169
  }
1120
- setupWebSocketHandlers(config) {
1121
- if (!this.webSocket)
1170
+ attachHandlers() {
1171
+ if (!this.socket)
1122
1172
  return;
1123
- this.webSocket.onopen = () => {
1124
- console.log('[WebSocketService] Connected to:', config.url);
1173
+ this.socket.onopen = () => {
1125
1174
  this.reconnectAttempts = 0;
1126
- this.updateStatus({
1127
- connected: true,
1128
- connecting: false,
1129
- reconnecting: false,
1130
- reconnectAttempts: 0,
1131
- });
1132
- // Start heartbeat if configured
1133
- if (config.heartbeatInterval && config.heartbeatMessage) {
1134
- this.startHeartbeat(config);
1135
- }
1175
+ this.updateStatus({ state: 'open', error: null, reconnectAttempts: 0 });
1176
+ this.startHeartbeat();
1136
1177
  };
1137
- this.webSocket.onclose = (event) => {
1138
- console.log('[WebSocketService] Connection closed:', event.code, event.reason);
1178
+ this.socket.onclose = (event) => {
1179
+ this.stopHeartbeat();
1180
+ if (this.disposed)
1181
+ return;
1139
1182
  this.updateStatus({
1140
- connected: false,
1141
- connecting: false,
1142
- reconnecting: false,
1143
- reconnectAttempts: this.reconnectAttempts,
1183
+ state: 'closed',
1184
+ error: event.wasClean ? null : `closed: ${event.code} ${event.reason}`,
1144
1185
  });
1145
- // Attempt reconnection if not a clean close and reconnect is enabled
1146
- if (!event.wasClean && config.reconnectInterval && config.maxReconnectAttempts) {
1147
- this.attemptReconnect(config);
1186
+ if (!event.wasClean) {
1187
+ this.scheduleReconnect();
1148
1188
  }
1149
1189
  };
1150
- this.webSocket.onerror = (error) => {
1151
- console.error('[WebSocketService] WebSocket error:', error);
1152
- this.updateStatus({
1153
- connected: false,
1154
- connecting: false,
1155
- reconnecting: false,
1156
- error: 'WebSocket connection error',
1157
- reconnectAttempts: this.reconnectAttempts,
1158
- });
1190
+ this.socket.onerror = () => {
1191
+ this.updateStatus({ error: 'WebSocket connection error' });
1159
1192
  };
1160
- this.webSocket.onmessage = (event) => {
1161
- try {
1162
- const message = JSON.parse(event.data);
1163
- this.messageSubject.next(message);
1164
- }
1165
- catch (error) {
1166
- console.error('[WebSocketService] Error parsing message:', error);
1167
- }
1193
+ this.socket.onmessage = (event) => {
1194
+ this.handleIncoming(event.data);
1168
1195
  };
1169
1196
  }
1170
- startHeartbeat(config) {
1171
- this.heartbeatTimer = setInterval(() => {
1172
- if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
1173
- this.send({
1174
- type: 'heartbeat',
1175
- data: config.heartbeatMessage,
1176
- });
1177
- }
1178
- }, config.heartbeatInterval);
1197
+ handleIncoming(raw) {
1198
+ let message;
1199
+ try {
1200
+ const text = typeof raw === 'string' ? raw : String(raw);
1201
+ message = JSON.parse(text);
1202
+ }
1203
+ catch (error) {
1204
+ this.logger.warn('[websocket] Failed to parse incoming message');
1205
+ this.logger.error('[websocket] parse error', error);
1206
+ return;
1207
+ }
1208
+ const correlationId = message.correlationId ?? message.id;
1209
+ if (correlationId && this.pendingRequests.has(correlationId)) {
1210
+ const pending = this.pendingRequests.get(correlationId);
1211
+ clearTimeout(pending.timer);
1212
+ this.pendingRequests.delete(correlationId);
1213
+ pending.resolve(message.data);
1214
+ return;
1215
+ }
1216
+ this._messages$.next(message);
1179
1217
  }
1180
- attemptReconnect(config) {
1181
- if (this.reconnectAttempts >= (config.maxReconnectAttempts || 5)) {
1182
- console.log('[WebSocketService] Max reconnect attempts reached');
1218
+ scheduleReconnect() {
1219
+ if (this.disposed)
1220
+ return;
1221
+ const interval = this.config.reconnectInterval ?? 0;
1222
+ const maxAttempts = this.config.maxReconnectAttempts ?? 0;
1223
+ if (interval <= 0 || maxAttempts <= 0)
1224
+ return;
1225
+ if (this.reconnectAttempts >= maxAttempts) {
1226
+ this.updateStatus({
1227
+ state: 'closed',
1228
+ error: `Max reconnect attempts (${maxAttempts}) reached`,
1229
+ });
1183
1230
  return;
1184
1231
  }
1185
- this.reconnectAttempts++;
1232
+ this.reconnectAttempts += 1;
1233
+ const delay = WebSocketClient.computeBackoffDelay(this.reconnectAttempts, interval, this.config.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_DELAY);
1186
1234
  this.updateStatus({
1187
- connected: false,
1188
- connecting: false,
1189
- reconnecting: true,
1235
+ state: 'reconnecting',
1190
1236
  reconnectAttempts: this.reconnectAttempts,
1191
1237
  });
1192
1238
  this.reconnectTimer = setTimeout(() => {
1193
- console.log(`[WebSocketService] Reconnect attempt ${this.reconnectAttempts}`);
1194
- this.connect(config);
1195
- }, config.reconnectInterval || 3000);
1239
+ this.reconnectTimer = null;
1240
+ this.openSocket();
1241
+ }, delay);
1196
1242
  }
1197
- updateStatus(status) {
1198
- const currentStatus = this.getCurrentStatus();
1199
- const newStatus = { ...currentStatus, ...status };
1200
- this.statusSubject.next(newStatus);
1243
+ /**
1244
+ * Exponential backoff with full jitter:
1245
+ * baseDelay = min(maxDelay, interval * 2^(attempt - 1))
1246
+ * delay = random(0, baseDelay)
1247
+ */
1248
+ static computeBackoffDelay(attempt, interval, maxDelay) {
1249
+ const exp = Math.min(maxDelay, interval * Math.pow(2, attempt - 1));
1250
+ return Math.floor(Math.random() * exp);
1201
1251
  }
1202
- getCurrentStatus() {
1203
- return {
1204
- connected: this.webSocket?.readyState === WebSocket.OPEN,
1205
- connecting: false,
1206
- reconnecting: false,
1207
- reconnectAttempts: this.reconnectAttempts,
1208
- };
1252
+ startHeartbeat() {
1253
+ const { heartbeatInterval, heartbeatMessage } = this.config;
1254
+ if (!heartbeatInterval || heartbeatMessage === undefined)
1255
+ return;
1256
+ this.stopHeartbeat();
1257
+ this.heartbeatTimer = setInterval(() => {
1258
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
1259
+ try {
1260
+ this.send({ type: 'heartbeat', data: heartbeatMessage });
1261
+ }
1262
+ catch (error) {
1263
+ this.logger.warn('[websocket] heartbeat send failed');
1264
+ this.logger.error('[websocket] heartbeat error', error);
1265
+ }
1266
+ }
1267
+ }, heartbeatInterval);
1209
1268
  }
1210
- // Direct access to native WebSocket
1211
- getNativeWebSocket() {
1212
- return this.webSocket;
1269
+ stopHeartbeat() {
1270
+ if (this.heartbeatTimer) {
1271
+ clearInterval(this.heartbeatTimer);
1272
+ this.heartbeatTimer = null;
1273
+ }
1213
1274
  }
1214
- isConnected() {
1215
- return this.webSocket?.readyState === WebSocket.OPEN;
1275
+ clearTimers() {
1276
+ if (this.reconnectTimer) {
1277
+ clearTimeout(this.reconnectTimer);
1278
+ this.reconnectTimer = null;
1279
+ }
1280
+ this.stopHeartbeat();
1216
1281
  }
1217
- getReadyState() {
1218
- return this.webSocket?.readyState ?? WebSocket.CLOSED;
1282
+ rejectAllPending(reason) {
1283
+ this.pendingRequests.forEach((entry) => {
1284
+ clearTimeout(entry.timer);
1285
+ entry.reject(reason);
1286
+ });
1287
+ this.pendingRequests.clear();
1288
+ }
1289
+ updateStatus(partial) {
1290
+ this._status.update((current) => ({ ...current, ...partial }));
1291
+ }
1292
+ generateId() {
1293
+ if (typeof globalThis.crypto?.randomUUID === 'function') {
1294
+ return globalThis.crypto.randomUUID();
1295
+ }
1296
+ return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
1219
1297
  }
1220
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1221
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService });
1222
1298
  }
1223
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, decorators: [{
1224
- type: Injectable
1225
- }] });
1226
1299
 
1227
- class WebWorkerService extends BrowserApiBaseService {
1228
- destroyRef = inject(DestroyRef);
1229
- workers = new Map();
1230
- workerStatuses = new Map();
1231
- workerMessages = new Map();
1232
- currentWorkerStatuses = new Map();
1300
+ let legacyDeprecationLogged = false;
1301
+ /**
1302
+ * Service that creates and tracks `WebSocketClient` instances.
1303
+ *
1304
+ * Preferred usage:
1305
+ * ```ts
1306
+ * const ws = inject(WebSocketService);
1307
+ * const client = ws.createClient({ url: 'wss://...' });
1308
+ * effect(() => console.log(client.status()));
1309
+ * await client.request('ping', {});
1310
+ * ```
1311
+ *
1312
+ * Legacy usage (`connect()` returning Observable) is preserved for one minor cycle
1313
+ * and will be removed in v22.
1314
+ */
1315
+ class WebSocketService extends BrowserApiBaseService {
1316
+ wsLogger = inject(BROWSER_API_LOGGER);
1317
+ clients = new Set();
1318
+ _cleanup = this.destroyRef.onDestroy(() => this.disposeAll());
1319
+ /** Legacy single-connection holder used by deprecated `connect()`/`send()` API. */
1320
+ legacyClient = null;
1233
1321
  getApiName() {
1234
- return 'webworker';
1322
+ return 'websocket';
1235
1323
  }
1236
- ensureWorkerSupport() {
1237
- if (typeof Worker === 'undefined') {
1238
- throw new Error('Web Workers not supported in this browser');
1324
+ ensureSupported() {
1325
+ super.ensureSupported();
1326
+ if (typeof WebSocket === 'undefined') {
1327
+ throw new Error('WebSocket API not supported in this browser');
1239
1328
  }
1240
1329
  }
1241
- createWorker(name, scriptUrl) {
1242
- this.ensureWorkerSupport();
1330
+ /**
1331
+ * Create a new WebSocket client. The client owns one connection and is the recommended
1332
+ * surface for all interactions (status signal, request/response, reconnect, etc.).
1333
+ *
1334
+ * The returned client is automatically disposed when the host injector is destroyed.
1335
+ */
1336
+ createClient(config) {
1337
+ this.ensureSupported();
1338
+ const client = new WebSocketClient(config, this.wsLogger, this.destroyRef);
1339
+ this.clients.add(client);
1340
+ return client;
1341
+ }
1342
+ /** Dispose every client created via `createClient()` (also called automatically on destroy). */
1343
+ disposeAll() {
1344
+ for (const client of this.clients) {
1345
+ client.close();
1346
+ }
1347
+ this.clients.clear();
1348
+ if (this.legacyClient) {
1349
+ this.legacyClient.close();
1350
+ this.legacyClient = null;
1351
+ }
1352
+ }
1353
+ // ---------- legacy API (deprecated) ----------
1354
+ /**
1355
+ * @deprecated Use {@link createClient} which returns a `WebSocketClient` exposing a
1356
+ * status signal, request/response, and proper reconnect. This wrapper will be removed
1357
+ * in v22.
1358
+ */
1359
+ connect(config) {
1360
+ this.ensureSupported();
1361
+ this.warnLegacyOnce();
1243
1362
  return new Observable((observer) => {
1244
- if (this.workers.has(name)) {
1245
- observer.next(this.currentWorkerStatuses.get(name));
1363
+ if (this.legacyClient) {
1364
+ this.legacyClient.close();
1365
+ }
1366
+ const client = new WebSocketClient(config, this.wsLogger);
1367
+ this.legacyClient = client;
1368
+ const sub = toObservableLike(client).subscribe({
1369
+ next: (status) => observer.next(status),
1370
+ error: (err) => observer.error(err),
1371
+ });
1372
+ return () => {
1373
+ sub.unsubscribe();
1374
+ client.close();
1375
+ if (this.legacyClient === client) {
1376
+ this.legacyClient = null;
1377
+ }
1378
+ };
1379
+ });
1380
+ }
1381
+ /** @deprecated Use {@link createClient} and call `client.close()`. */
1382
+ disconnect() {
1383
+ if (this.legacyClient) {
1384
+ this.legacyClient.close();
1385
+ this.legacyClient = null;
1386
+ }
1387
+ }
1388
+ /** @deprecated Use the client returned by {@link createClient}. */
1389
+ send(message) {
1390
+ if (!this.legacyClient) {
1391
+ throw new Error('No active legacy WebSocket. Call connect() first or use createClient().');
1392
+ }
1393
+ this.legacyClient.send(message);
1394
+ }
1395
+ /** @deprecated Use the client returned by {@link createClient}. */
1396
+ sendRaw(data) {
1397
+ if (!this.legacyClient) {
1398
+ throw new Error('No active legacy WebSocket. Call connect() first or use createClient().');
1399
+ }
1400
+ this.legacyClient.sendRaw(data);
1401
+ }
1402
+ /** @deprecated Use `client.status` from {@link createClient}. */
1403
+ getStatus() {
1404
+ return new Observable((observer) => {
1405
+ if (!this.legacyClient) {
1406
+ observer.next({
1407
+ connected: false,
1408
+ connecting: false,
1409
+ reconnecting: false,
1410
+ reconnectAttempts: 0,
1411
+ });
1246
1412
  return () => {
1247
- // No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
1413
+ // No-op
1248
1414
  };
1249
1415
  }
1250
- try {
1251
- const worker = new Worker(scriptUrl);
1252
- this.workers.set(name, worker);
1416
+ const sub = toObservableLike(this.legacyClient).subscribe((status) => observer.next(status));
1417
+ return () => sub.unsubscribe();
1418
+ });
1419
+ }
1420
+ /** @deprecated Use `client.messages$` from {@link createClient}. */
1421
+ getMessages() {
1422
+ if (!this.legacyClient) {
1423
+ return new Observable(() => {
1424
+ // No-op stream until connected.
1425
+ });
1426
+ }
1427
+ return this.legacyClient.messages$;
1428
+ }
1429
+ /** @deprecated Use `client.messagesByType()` from {@link createClient}. */
1430
+ getMessagesByType(type) {
1431
+ if (!this.legacyClient) {
1432
+ return new Observable(() => {
1433
+ // No-op stream until connected.
1434
+ });
1435
+ }
1436
+ return this.legacyClient.messagesByType(type);
1437
+ }
1438
+ /** @deprecated Use the client returned by {@link createClient}. */
1439
+ getNativeWebSocket() {
1440
+ return this.legacyClient?.getNativeSocket() ?? null;
1441
+ }
1442
+ /** @deprecated Use `client.status()` from {@link createClient}. */
1443
+ isConnected() {
1444
+ return this.legacyClient?.status().state === 'open';
1445
+ }
1446
+ /** @deprecated Use the native socket via `client.getNativeSocket()`. */
1447
+ getReadyState() {
1448
+ return this.legacyClient?.getNativeSocket()?.readyState ?? WebSocket.CLOSED;
1449
+ }
1450
+ warnLegacyOnce() {
1451
+ if (legacyDeprecationLogged)
1452
+ return;
1453
+ legacyDeprecationLogged = true;
1454
+ this.wsLogger.warn('[websocket] WebSocketService.connect() is deprecated. Use WebSocketService.createClient() ' +
1455
+ 'which returns a WebSocketClient with a status signal, request/response, and proper reconnect. ' +
1456
+ 'The legacy API will be removed in v22.');
1457
+ }
1458
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1459
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService });
1460
+ }
1461
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, decorators: [{
1462
+ type: Injectable
1463
+ }] });
1464
+ /**
1465
+ * Build a stream of legacy `WebSocketStatus` snapshots from a v2 client. Used to keep the
1466
+ * deprecated `connect()` API behaving like before (Observable of legacy status).
1467
+ */
1468
+ function toObservableLike(client) {
1469
+ return new Observable((observer) => {
1470
+ const emit = () => {
1471
+ const v2 = client.status();
1472
+ observer.next({
1473
+ connected: v2.state === 'open',
1474
+ connecting: v2.state === 'connecting',
1475
+ reconnecting: v2.state === 'reconnecting',
1476
+ error: v2.error ?? undefined,
1477
+ reconnectAttempts: v2.reconnectAttempts,
1478
+ });
1479
+ };
1480
+ emit();
1481
+ const id = setInterval(emit, 100);
1482
+ return () => clearInterval(id);
1483
+ });
1484
+ }
1485
+
1486
+ class WebWorkerService extends BrowserApiBaseService {
1487
+ workers = new Map();
1488
+ workerStatuses = new Map();
1489
+ workerMessages = new Map();
1490
+ currentWorkerStatuses = new Map();
1491
+ _cleanup = this.destroyRef.onDestroy(() => this.terminateAllWorkers());
1492
+ getApiName() {
1493
+ return 'webworker';
1494
+ }
1495
+ ensureSupported() {
1496
+ super.ensureSupported();
1497
+ if (typeof Worker === 'undefined') {
1498
+ throw new Error('Web Workers not supported in this browser');
1499
+ }
1500
+ }
1501
+ createWorker(name, scriptUrl) {
1502
+ this.ensureSupported();
1503
+ return new Observable((observer) => {
1504
+ if (this.workers.has(name)) {
1505
+ observer.next(this.currentWorkerStatuses.get(name));
1506
+ return () => {
1507
+ // No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
1508
+ };
1509
+ }
1510
+ try {
1511
+ const worker = new Worker(scriptUrl);
1512
+ this.workers.set(name, worker);
1253
1513
  this.setupWorker(name, worker);
1254
1514
  const status = {
1255
1515
  initialized: true,
@@ -1261,7 +1521,7 @@ class WebWorkerService extends BrowserApiBaseService {
1261
1521
  observer.next(status);
1262
1522
  }
1263
1523
  catch (error) {
1264
- console.error(`[WebWorkerService] Failed to create worker ${name}:`, error);
1524
+ this.logError(`Failed to create worker ${name}:`, error);
1265
1525
  const status = {
1266
1526
  initialized: false,
1267
1527
  running: false,
@@ -1295,7 +1555,7 @@ class WebWorkerService extends BrowserApiBaseService {
1295
1555
  postMessage(workerName, task) {
1296
1556
  const worker = this.workers.get(workerName);
1297
1557
  if (!worker) {
1298
- console.error(`[WebWorkerService] Worker ${workerName} not found`);
1558
+ this.logError(`Worker ${workerName} not found`);
1299
1559
  return;
1300
1560
  }
1301
1561
  try {
@@ -1313,7 +1573,7 @@ class WebWorkerService extends BrowserApiBaseService {
1313
1573
  }
1314
1574
  }
1315
1575
  catch (error) {
1316
- console.error(`[WebWorkerService] Failed to post message to worker ${workerName}:`, error);
1576
+ this.logError(`Failed to post message to worker ${workerName}:`, error);
1317
1577
  }
1318
1578
  }
1319
1579
  getMessages(workerName) {
@@ -1352,7 +1612,7 @@ class WebWorkerService extends BrowserApiBaseService {
1352
1612
  this.workerMessages.get(name).next(message);
1353
1613
  };
1354
1614
  worker.onerror = (error) => {
1355
- console.error(`[WebWorkerService] Worker ${name} error:`, error);
1615
+ this.logError(`Worker ${name} error:`, error);
1356
1616
  const status = {
1357
1617
  initialized: true,
1358
1618
  running: false,
@@ -1724,7 +1984,7 @@ class ScreenWakeLockService extends BrowserApiBaseService {
1724
1984
  return 'screen-wake-lock';
1725
1985
  }
1726
1986
  isSupported() {
1727
- return isPlatformBrowser(this.platformId) && 'wakeLock' in navigator;
1987
+ return this.isBrowserEnvironment() && 'wakeLock' in navigator;
1728
1988
  }
1729
1989
  get isActive() {
1730
1990
  return this.sentinel !== null && !this.sentinel.released;
@@ -1745,7 +2005,7 @@ class ScreenWakeLockService extends BrowserApiBaseService {
1745
2005
  return { active: true, type, released: false };
1746
2006
  }
1747
2007
  catch (error) {
1748
- console.error('[ScreenWakeLockService] Failed to acquire wake lock:', error);
2008
+ this.logError('Failed to acquire wake lock:', error);
1749
2009
  throw this.createError('Failed to acquire wake lock', error);
1750
2010
  }
1751
2011
  }
@@ -1961,10 +2221,8 @@ class FileSystemAccessService extends BrowserApiBaseService {
1961
2221
  get win() {
1962
2222
  return window;
1963
2223
  }
1964
- ensureSupport() {
1965
- if (this.isServerEnvironment()) {
1966
- throw new Error('File System Access API not available in server environment');
1967
- }
2224
+ ensureSupported() {
2225
+ super.ensureSupported();
1968
2226
  if (!('showOpenFilePicker' in window)) {
1969
2227
  throw new Error('File System Access API not supported in this browser');
1970
2228
  }
@@ -1973,7 +2231,7 @@ class FileSystemAccessService extends BrowserApiBaseService {
1973
2231
  }
1974
2232
  }
1975
2233
  async openFile(options = {}) {
1976
- this.ensureSupport();
2234
+ this.ensureSupported();
1977
2235
  try {
1978
2236
  const handles = await this.win.showOpenFilePicker(options);
1979
2237
  return Promise.all(handles.map((h) => h.getFile()));
@@ -1982,12 +2240,12 @@ class FileSystemAccessService extends BrowserApiBaseService {
1982
2240
  if (error instanceof DOMException && error.name === 'AbortError') {
1983
2241
  return [];
1984
2242
  }
1985
- console.error('[FileSystemAccessService] Error opening file:', error);
2243
+ this.logError('Error opening file:', error);
1986
2244
  throw error;
1987
2245
  }
1988
2246
  }
1989
2247
  async saveFile(content, options = {}) {
1990
- this.ensureSupport();
2248
+ this.ensureSupported();
1991
2249
  if (!this.win.showSaveFilePicker) {
1992
2250
  throw new Error('showSaveFilePicker not supported');
1993
2251
  }
@@ -2001,12 +2259,12 @@ class FileSystemAccessService extends BrowserApiBaseService {
2001
2259
  if (error instanceof DOMException && error.name === 'AbortError') {
2002
2260
  return;
2003
2261
  }
2004
- console.error('[FileSystemAccessService] Error saving file:', error);
2262
+ this.logError('Error saving file:', error);
2005
2263
  throw error;
2006
2264
  }
2007
2265
  }
2008
2266
  async openDirectory(options = {}) {
2009
- this.ensureSupport();
2267
+ this.ensureSupported();
2010
2268
  if (!this.win.showDirectoryPicker) {
2011
2269
  throw new Error('showDirectoryPicker not supported');
2012
2270
  }
@@ -2017,7 +2275,7 @@ class FileSystemAccessService extends BrowserApiBaseService {
2017
2275
  if (error instanceof DOMException && error.name === 'AbortError') {
2018
2276
  return null;
2019
2277
  }
2020
- console.error('[FileSystemAccessService] Error opening directory:', error);
2278
+ this.logError('Error opening directory:', error);
2021
2279
  throw error;
2022
2280
  }
2023
2281
  }
@@ -2084,7 +2342,7 @@ class MediaRecorderService extends BrowserApiBaseService {
2084
2342
  this.recorder.start(timeslice);
2085
2343
  }
2086
2344
  catch (error) {
2087
- console.error('[MediaRecorderService] Failed to start recording:', error);
2345
+ this.logError('Failed to start recording:', error);
2088
2346
  throw this.createError('Failed to start recording', error);
2089
2347
  }
2090
2348
  }
@@ -2170,7 +2428,7 @@ class ServerSentEventsService extends ConnectionRegistryBaseService {
2170
2428
  observer.error(new Error('SSE connection closed unexpectedly'));
2171
2429
  }
2172
2430
  else {
2173
- console.warn(`[${this.getApiName()}] SSE connection error, reconnecting...`, event);
2431
+ this.logWarn('SSE connection error, reconnecting...');
2174
2432
  }
2175
2433
  };
2176
2434
  source.addEventListener('message', messageHandler);
@@ -2432,106 +2690,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
2432
2690
  type: Injectable
2433
2691
  }] });
2434
2692
 
2435
- function getIdleDetectorClass$1() {
2436
- return window.IdleDetector;
2437
- }
2438
- class IdleDetectorService extends BrowserApiBaseService {
2439
- getApiName() {
2440
- return 'idle-detector';
2441
- }
2442
- isSupported() {
2443
- return this.isBrowserEnvironment() && 'IdleDetector' in window;
2444
- }
2445
- async requestPermission() {
2446
- if (!this.isSupported()) {
2447
- throw new Error('IdleDetector API not supported');
2448
- }
2449
- return getIdleDetectorClass$1().requestPermission();
2450
- }
2451
- watch(options = {}) {
2452
- if (!this.isSupported()) {
2453
- return new Observable((o) => o.error(new Error('IdleDetector API not supported')));
2454
- }
2455
- return new Observable((subscriber) => {
2456
- const abortController = new AbortController();
2457
- const detector = new (getIdleDetectorClass$1())();
2458
- detector.addEventListener('change', () => {
2459
- subscriber.next({
2460
- user: detector.userState,
2461
- screen: detector.screenState,
2462
- });
2463
- });
2464
- detector
2465
- .start({
2466
- threshold: options.threshold ?? 60_000,
2467
- signal: abortController.signal,
2468
- })
2469
- .catch((err) => subscriber.error(err));
2470
- return () => abortController.abort();
2471
- });
2472
- }
2473
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IdleDetectorService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2474
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IdleDetectorService });
2475
- }
2476
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IdleDetectorService, decorators: [{
2477
- type: Injectable
2478
- }] });
2479
-
2480
- function getEyeDropperClass() {
2481
- return window.EyeDropper;
2482
- }
2483
- class EyeDropperService extends BrowserApiBaseService {
2484
- getApiName() {
2485
- return 'eye-dropper';
2486
- }
2487
- isSupported() {
2488
- return this.isBrowserEnvironment() && 'EyeDropper' in window;
2489
- }
2490
- async open(signal) {
2491
- if (!this.isSupported()) {
2492
- throw new Error('EyeDropper API not supported');
2493
- }
2494
- const dropper = new (getEyeDropperClass())();
2495
- return dropper.open(signal ? { signal } : undefined);
2496
- }
2497
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: EyeDropperService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2498
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: EyeDropperService });
2499
- }
2500
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: EyeDropperService, decorators: [{
2501
- type: Injectable
2502
- }] });
2503
-
2504
- function getBarcodeDetectorClass() {
2505
- return window.BarcodeDetector;
2506
- }
2507
- class BarcodeDetectorService extends BrowserApiBaseService {
2508
- getApiName() {
2509
- return 'barcode-detector';
2510
- }
2511
- isSupported() {
2512
- return this.isBrowserEnvironment() && 'BarcodeDetector' in window;
2513
- }
2514
- async getSupportedFormats() {
2515
- if (!this.isSupported())
2516
- return [];
2517
- return getBarcodeDetectorClass().getSupportedFormats();
2518
- }
2519
- async detect(image, formats) {
2520
- if (!this.isSupported()) {
2521
- throw new Error('BarcodeDetector API not supported');
2522
- }
2523
- const detector = formats
2524
- ? new (getBarcodeDetectorClass())({ formats })
2525
- : new (getBarcodeDetectorClass())();
2526
- return detector.detect(image);
2527
- }
2528
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BarcodeDetectorService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2529
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BarcodeDetectorService });
2530
- }
2531
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BarcodeDetectorService, decorators: [{
2532
- type: Injectable
2533
- }] });
2534
-
2535
2693
  class WebAudioService extends BrowserApiBaseService {
2536
2694
  getApiName() {
2537
2695
  return 'web-audio';
@@ -2741,301 +2899,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
2741
2899
  type: Injectable
2742
2900
  }] });
2743
2901
 
2744
- function getBluetooth() {
2745
- return navigator.bluetooth;
2746
- }
2747
- class WebBluetoothService extends BrowserApiBaseService {
2748
- getApiName() {
2749
- return 'web-bluetooth';
2750
- }
2751
- isSupported() {
2752
- return this.isBrowserEnvironment() && !!getBluetooth();
2753
- }
2754
- async requestDevice(options = { acceptAllDevices: true }) {
2755
- if (!this.isSupported()) {
2756
- throw new Error('Web Bluetooth API not supported');
2757
- }
2758
- return getBluetooth().requestDevice(options);
2759
- }
2760
- async connect(device) {
2761
- if (!device.gatt) {
2762
- throw new Error('GATT server not available on this device');
2763
- }
2764
- return device.gatt.connect();
2765
- }
2766
- disconnect(device) {
2767
- device.gatt?.disconnect();
2768
- }
2769
- watchDisconnection(device) {
2770
- return new Observable((subscriber) => {
2771
- const handler = () => subscriber.next();
2772
- device.addEventListener('gattserverdisconnected', handler);
2773
- return () => device.removeEventListener('gattserverdisconnected', handler);
2774
- });
2775
- }
2776
- async readCharacteristic(server, serviceUuid, characteristicUuid) {
2777
- const service = await server.getPrimaryService(serviceUuid);
2778
- const characteristic = await service.getCharacteristic(characteristicUuid);
2779
- return characteristic.readValue();
2780
- }
2781
- async writeCharacteristic(server, serviceUuid, characteristicUuid, value) {
2782
- const service = await server.getPrimaryService(serviceUuid);
2783
- const characteristic = await service.getCharacteristic(characteristicUuid);
2784
- await characteristic.writeValue(value);
2785
- }
2786
- watchCharacteristic(server, serviceUuid, characteristicUuid) {
2787
- return new Observable((subscriber) => {
2788
- let characteristic;
2789
- server
2790
- .getPrimaryService(serviceUuid)
2791
- .then((service) => service.getCharacteristic(characteristicUuid))
2792
- .then((char) => {
2793
- characteristic = char;
2794
- const handler = (event) => {
2795
- const target = event.target;
2796
- if (target.value)
2797
- subscriber.next(target.value);
2798
- };
2799
- characteristic.addEventListener('characteristicvaluechanged', handler);
2800
- return characteristic.startNotifications();
2801
- })
2802
- .catch((err) => subscriber.error(err));
2803
- return () => {
2804
- characteristic?.stopNotifications().catch(() => { });
2805
- };
2806
- });
2807
- }
2808
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebBluetoothService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2809
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebBluetoothService });
2810
- }
2811
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebBluetoothService, decorators: [{
2812
- type: Injectable
2813
- }] });
2814
-
2815
- function getUsb() {
2816
- return navigator.usb;
2817
- }
2818
- class WebUsbService extends BrowserApiBaseService {
2819
- getApiName() {
2820
- return 'web-usb';
2821
- }
2822
- isSupported() {
2823
- return this.isBrowserEnvironment() && !!getUsb();
2824
- }
2825
- async requestDevice(filters = []) {
2826
- if (!this.isSupported()) {
2827
- throw new Error('WebUSB API not supported');
2828
- }
2829
- return getUsb().requestDevice({ filters });
2830
- }
2831
- async getDevices() {
2832
- if (!this.isSupported())
2833
- return [];
2834
- return getUsb().getDevices();
2835
- }
2836
- async open(device) {
2837
- await device.open();
2838
- }
2839
- async close(device) {
2840
- await device.close();
2841
- }
2842
- async selectConfiguration(device, configurationValue) {
2843
- await device.selectConfiguration(configurationValue);
2844
- }
2845
- async claimInterface(device, interfaceNumber) {
2846
- await device.claimInterface(interfaceNumber);
2847
- }
2848
- async releaseInterface(device, interfaceNumber) {
2849
- await device.releaseInterface(interfaceNumber);
2850
- }
2851
- async transferIn(device, endpointNumber, length) {
2852
- return device.transferIn(endpointNumber, length);
2853
- }
2854
- async transferOut(device, endpointNumber, data) {
2855
- return device.transferOut(endpointNumber, data);
2856
- }
2857
- watchConnection() {
2858
- if (!this.isSupported()) {
2859
- return new Observable((o) => o.error(new Error('WebUSB API not supported')));
2860
- }
2861
- return new Observable((subscriber) => {
2862
- const usb = getUsb();
2863
- const onConnect = (e) => subscriber.next({ device: e.device, type: 'connect' });
2864
- const onDisconnect = (e) => subscriber.next({ device: e.device, type: 'disconnect' });
2865
- usb.addEventListener('connect', onConnect);
2866
- usb.addEventListener('disconnect', onDisconnect);
2867
- return () => {
2868
- usb.removeEventListener('connect', onConnect);
2869
- usb.removeEventListener('disconnect', onDisconnect);
2870
- };
2871
- });
2872
- }
2873
- getDeviceInfo(device) {
2874
- return {
2875
- vendorId: device.vendorId,
2876
- productId: device.productId,
2877
- productName: device.productName,
2878
- manufacturerName: device.manufacturerName,
2879
- serialNumber: device.serialNumber,
2880
- opened: device.opened,
2881
- };
2882
- }
2883
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebUsbService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2884
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebUsbService });
2885
- }
2886
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebUsbService, decorators: [{
2887
- type: Injectable
2888
- }] });
2889
-
2890
- function getNdefReaderClass() {
2891
- return window.NDEFReader;
2892
- }
2893
- class WebNfcService extends BrowserApiBaseService {
2894
- getApiName() {
2895
- return 'web-nfc';
2896
- }
2897
- isSupported() {
2898
- return this.isBrowserEnvironment() && 'NDEFReader' in window;
2899
- }
2900
- scan() {
2901
- if (!this.isSupported()) {
2902
- return new Observable((o) => o.error(new Error('Web NFC API not supported')));
2903
- }
2904
- return new Observable((subscriber) => {
2905
- const abortController = new AbortController();
2906
- const reader = new (getNdefReaderClass())();
2907
- const onReading = (event) => {
2908
- const e = event;
2909
- subscriber.next({
2910
- serialNumber: e.serialNumber,
2911
- message: e.message,
2912
- });
2913
- };
2914
- const onError = (event) => {
2915
- subscriber.error(event.error ?? new Error('NFC read error'));
2916
- };
2917
- reader.addEventListener('reading', onReading);
2918
- reader.addEventListener('readingerror', onError);
2919
- reader
2920
- .scan({ signal: abortController.signal })
2921
- .catch((err) => subscriber.error(err));
2922
- return () => abortController.abort();
2923
- });
2924
- }
2925
- async write(message, options) {
2926
- if (!this.isSupported()) {
2927
- throw new Error('Web NFC API not supported');
2928
- }
2929
- const reader = new (getNdefReaderClass())();
2930
- await reader.write(message, options);
2931
- }
2932
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebNfcService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2933
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebNfcService });
2934
- }
2935
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebNfcService, decorators: [{
2936
- type: Injectable
2937
- }] });
2938
-
2939
- class PaymentRequestService extends BrowserApiBaseService {
2940
- getApiName() {
2941
- return 'payment-request';
2942
- }
2943
- isSupported() {
2944
- return this.isBrowserEnvironment() && 'PaymentRequest' in window;
2945
- }
2946
- async canMakePayment(methods, details) {
2947
- if (!this.isSupported())
2948
- return false;
2949
- const request = new PaymentRequest(methods, details);
2950
- return request.canMakePayment();
2951
- }
2952
- async show(methods, details, options) {
2953
- if (!this.isSupported()) {
2954
- throw new Error('Payment Request API not supported');
2955
- }
2956
- const request = new PaymentRequest(methods, details, options);
2957
- const response = await request.show();
2958
- const result = {
2959
- methodName: response.methodName,
2960
- details: response.details,
2961
- payerName: response.payerName ?? null,
2962
- payerEmail: response.payerEmail ?? null,
2963
- payerPhone: response.payerPhone ?? null,
2964
- };
2965
- await response.complete('success');
2966
- return result;
2967
- }
2968
- async abort(methods, details) {
2969
- if (!this.isSupported())
2970
- return;
2971
- const request = new PaymentRequest(methods, details);
2972
- await request.abort();
2973
- }
2974
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PaymentRequestService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2975
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PaymentRequestService });
2976
- }
2977
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PaymentRequestService, decorators: [{
2978
- type: Injectable
2979
- }] });
2980
-
2981
- class CredentialManagementService extends BrowserApiBaseService {
2982
- getApiName() {
2983
- return 'credential-management';
2984
- }
2985
- isSupported() {
2986
- return this.isBrowserEnvironment() && 'credentials' in navigator;
2987
- }
2988
- isPublicKeySupported() {
2989
- return this.isSupported() && 'PublicKeyCredential' in window;
2990
- }
2991
- async get(options) {
2992
- if (!this.isSupported()) {
2993
- throw new Error('Credential Management API not supported');
2994
- }
2995
- return navigator.credentials.get(options);
2996
- }
2997
- async store(credential) {
2998
- if (!this.isSupported()) {
2999
- throw new Error('Credential Management API not supported');
3000
- }
3001
- await navigator.credentials.store(credential);
3002
- }
3003
- async createPasswordCredential(data) {
3004
- if (!this.isSupported()) {
3005
- throw new Error('Credential Management API not supported');
3006
- }
3007
- return navigator.credentials.create({
3008
- password: data,
3009
- });
3010
- }
3011
- async createPublicKeyCredential(options) {
3012
- if (!this.isPublicKeySupported()) {
3013
- throw new Error('PublicKeyCredential API not supported');
3014
- }
3015
- return navigator.credentials.create({
3016
- publicKey: options,
3017
- });
3018
- }
3019
- async preventSilentAccess() {
3020
- if (!this.isSupported())
3021
- return;
3022
- await navigator.credentials.preventSilentAccess();
3023
- }
3024
- async isConditionalMediationAvailable() {
3025
- if (!this.isPublicKeySupported())
3026
- return false;
3027
- if ('isConditionalMediationAvailable' in PublicKeyCredential) {
3028
- return PublicKeyCredential.isConditionalMediationAvailable();
3029
- }
3030
- return false;
3031
- }
3032
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CredentialManagementService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
3033
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CredentialManagementService });
3034
- }
3035
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CredentialManagementService, decorators: [{
3036
- type: Injectable
3037
- }] });
3038
-
3039
2902
  // Common types for browser APIs
3040
2903
 
3041
2904
  function injectPageVisibility() {
@@ -3199,42 +3062,6 @@ function injectPerformanceObserver(config) {
3199
3062
  };
3200
3063
  }
3201
3064
 
3202
- function getIdleDetectorClass() {
3203
- return window.IdleDetector;
3204
- }
3205
- function injectIdleDetector(options = {}) {
3206
- const destroyRef = inject(DestroyRef);
3207
- const platformId = inject(PLATFORM_ID);
3208
- const defaultState = { user: 'active', screen: 'unlocked' };
3209
- const state = signal(defaultState, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
3210
- if (isPlatformBrowser(platformId) && 'IdleDetector' in window) {
3211
- const abortController = new AbortController();
3212
- const detector = new (getIdleDetectorClass())();
3213
- detector.addEventListener('change', () => {
3214
- state.set({
3215
- user: detector.userState,
3216
- screen: detector.screenState,
3217
- });
3218
- });
3219
- detector
3220
- .start({
3221
- threshold: options.threshold ?? 60_000,
3222
- signal: abortController.signal,
3223
- })
3224
- .catch(() => {
3225
- /* permission denied or unsupported — keep defaults */
3226
- });
3227
- destroyRef.onDestroy(() => abortController.abort());
3228
- }
3229
- return {
3230
- state: state.asReadonly(),
3231
- userState: computed(() => state().user),
3232
- screenState: computed(() => state().screen),
3233
- isUserIdle: computed(() => state().user === 'idle'),
3234
- isScreenLocked: computed(() => state().screen === 'locked'),
3235
- };
3236
- }
3237
-
3238
3065
  function injectGamepad(index, intervalMs = 16) {
3239
3066
  const destroyRef = inject(DestroyRef);
3240
3067
  const platformId = inject(PLATFORM_ID);
@@ -3365,216 +3192,203 @@ const permissionGuard = (permission) => {
3365
3192
  };
3366
3193
  };
3367
3194
 
3368
- const defaultBrowserWebApisConfig = {
3369
- enableCamera: true,
3370
- enableGeolocation: true,
3371
- enableNotifications: true,
3372
- enableClipboard: true,
3373
- enableBattery: false,
3374
- enableMediaDevices: true,
3375
- enableWebShare: false,
3376
- enableWebStorage: false,
3377
- enableWebSocket: false,
3378
- enableWebWorker: false,
3379
- enableIntersectionObserver: false,
3380
- enableResizeObserver: false,
3381
- enablePageVisibility: false,
3382
- enableBroadcastChannel: false,
3383
- enableNetworkInformation: false,
3384
- enableScreenWakeLock: false,
3385
- enableScreenOrientation: false,
3386
- enableFullscreen: false,
3387
- enableFileSystemAccess: false,
3388
- enableMediaRecorder: false,
3389
- enableServerSentEvents: false,
3390
- enableVibration: false,
3391
- enableSpeechSynthesis: false,
3392
- enableMutationObserver: false,
3393
- enablePerformanceObserver: false,
3394
- enableIdleDetector: false,
3395
- enableEyeDropper: false,
3396
- enableBarcodeDetector: false,
3397
- enableWebAudio: false,
3398
- enableGamepad: false,
3399
- enableWebBluetooth: false,
3400
- enableWebUsb: false,
3401
- enableWebNfc: false,
3402
- enablePaymentRequest: false,
3403
- enableCredentialManagement: false,
3404
- };
3405
- function provideBrowserWebApis(config = {}) {
3406
- const mergedConfig = { ...defaultBrowserWebApisConfig, ...config };
3407
- const providers = [PermissionsService];
3408
- const conditionalProviders = [
3409
- [mergedConfig.enableCamera, CameraService],
3410
- [mergedConfig.enableGeolocation, GeolocationService],
3411
- [mergedConfig.enableNotifications, NotificationService],
3412
- [mergedConfig.enableClipboard, ClipboardService],
3413
- [mergedConfig.enableMediaDevices, MediaDevicesService],
3414
- [mergedConfig.enableBattery, BatteryService],
3415
- [mergedConfig.enableWebShare, WebShareService],
3416
- [mergedConfig.enableWebStorage, WebStorageService],
3417
- [mergedConfig.enableWebSocket, WebSocketService],
3418
- [mergedConfig.enableWebWorker, WebWorkerService],
3419
- [mergedConfig.enableIntersectionObserver, IntersectionObserverService],
3420
- [mergedConfig.enableResizeObserver, ResizeObserverService],
3421
- [mergedConfig.enablePageVisibility, PageVisibilityService],
3422
- [mergedConfig.enableBroadcastChannel, BroadcastChannelService],
3423
- [mergedConfig.enableNetworkInformation, NetworkInformationService],
3424
- [mergedConfig.enableScreenWakeLock, ScreenWakeLockService],
3425
- [mergedConfig.enableScreenOrientation, ScreenOrientationService],
3426
- [mergedConfig.enableFullscreen, FullscreenService],
3427
- [mergedConfig.enableFileSystemAccess, FileSystemAccessService],
3428
- [mergedConfig.enableMediaRecorder, MediaRecorderService],
3429
- [mergedConfig.enableServerSentEvents, ServerSentEventsService],
3430
- [mergedConfig.enableVibration, VibrationService],
3431
- [mergedConfig.enableSpeechSynthesis, SpeechSynthesisService],
3432
- [mergedConfig.enableMutationObserver, MutationObserverService],
3433
- [mergedConfig.enablePerformanceObserver, PerformanceObserverService],
3434
- [mergedConfig.enableIdleDetector, IdleDetectorService],
3435
- [mergedConfig.enableEyeDropper, EyeDropperService],
3436
- [mergedConfig.enableBarcodeDetector, BarcodeDetectorService],
3437
- [mergedConfig.enableWebAudio, WebAudioService],
3438
- [mergedConfig.enableGamepad, GamepadService],
3439
- [mergedConfig.enableWebBluetooth, WebBluetoothService],
3440
- [mergedConfig.enableWebUsb, WebUsbService],
3441
- [mergedConfig.enableWebNfc, WebNfcService],
3442
- [mergedConfig.enablePaymentRequest, PaymentRequestService],
3443
- [mergedConfig.enableCredentialManagement, CredentialManagementService],
3444
- ];
3445
- for (const [enabled, provider] of conditionalProviders) {
3446
- if (enabled) {
3447
- providers.push(provider);
3448
- }
3449
- }
3450
- return makeEnvironmentProviders(providers);
3195
+ function providePermissions() {
3196
+ return makeEnvironmentProviders([PermissionsService]);
3451
3197
  }
3452
- // Feature-specific providers for tree-shaking
3198
+
3453
3199
  function provideCamera() {
3454
- return makeEnvironmentProviders([PermissionsService, CameraService]);
3200
+ return makeEnvironmentProviders([CameraService]);
3455
3201
  }
3202
+
3456
3203
  function provideGeolocation() {
3457
3204
  return makeEnvironmentProviders([PermissionsService, GeolocationService]);
3458
3205
  }
3206
+
3459
3207
  function provideNotifications() {
3460
3208
  return makeEnvironmentProviders([PermissionsService, NotificationService]);
3461
3209
  }
3210
+
3462
3211
  function provideClipboard() {
3463
- return makeEnvironmentProviders([PermissionsService, ClipboardService]);
3212
+ return makeEnvironmentProviders([ClipboardService]);
3464
3213
  }
3214
+
3465
3215
  function provideMediaDevices() {
3466
3216
  return makeEnvironmentProviders([PermissionsService, MediaDevicesService]);
3467
3217
  }
3218
+
3219
+ function provideScreenWakeLock() {
3220
+ return makeEnvironmentProviders([PermissionsService, ScreenWakeLockService]);
3221
+ }
3222
+
3223
+ function provideFileSystemAccess() {
3224
+ return makeEnvironmentProviders([PermissionsService, FileSystemAccessService]);
3225
+ }
3226
+
3227
+ function provideMediaRecorder() {
3228
+ return makeEnvironmentProviders([PermissionsService, MediaRecorderService]);
3229
+ }
3230
+
3468
3231
  function provideBattery() {
3469
3232
  return makeEnvironmentProviders([BatteryService]);
3470
3233
  }
3234
+
3471
3235
  function provideWebShare() {
3472
3236
  return makeEnvironmentProviders([WebShareService]);
3473
3237
  }
3238
+
3474
3239
  function provideWebStorage() {
3475
3240
  return makeEnvironmentProviders([WebStorageService]);
3476
3241
  }
3242
+
3477
3243
  function provideWebSocket() {
3478
3244
  return makeEnvironmentProviders([WebSocketService]);
3479
3245
  }
3246
+
3480
3247
  function provideWebWorker() {
3481
3248
  return makeEnvironmentProviders([WebWorkerService]);
3482
3249
  }
3483
- function providePermissions() {
3484
- return makeEnvironmentProviders([PermissionsService]);
3485
- }
3486
- // Combined providers for common use cases
3487
- function provideMediaApis() {
3488
- return makeEnvironmentProviders([PermissionsService, CameraService, MediaDevicesService]);
3489
- }
3490
- function provideLocationApis() {
3491
- return makeEnvironmentProviders([PermissionsService, GeolocationService]);
3492
- }
3493
- function provideStorageApis() {
3494
- return makeEnvironmentProviders([PermissionsService, ClipboardService, WebStorageService]);
3495
- }
3496
- function provideCommunicationApis() {
3497
- return makeEnvironmentProviders([
3498
- PermissionsService,
3499
- NotificationService,
3500
- WebShareService,
3501
- WebSocketService,
3502
- ]);
3503
- }
3250
+
3504
3251
  function provideIntersectionObserver() {
3505
3252
  return makeEnvironmentProviders([IntersectionObserverService]);
3506
3253
  }
3254
+
3507
3255
  function provideResizeObserver() {
3508
3256
  return makeEnvironmentProviders([ResizeObserverService]);
3509
3257
  }
3258
+
3510
3259
  function providePageVisibility() {
3511
3260
  return makeEnvironmentProviders([PageVisibilityService]);
3512
3261
  }
3262
+
3513
3263
  function provideBroadcastChannel() {
3514
3264
  return makeEnvironmentProviders([BroadcastChannelService]);
3515
3265
  }
3266
+
3516
3267
  function provideNetworkInformation() {
3517
3268
  return makeEnvironmentProviders([NetworkInformationService]);
3518
3269
  }
3519
- function provideScreenWakeLock() {
3520
- return makeEnvironmentProviders([PermissionsService, ScreenWakeLockService]);
3521
- }
3270
+
3522
3271
  function provideScreenOrientation() {
3523
3272
  return makeEnvironmentProviders([ScreenOrientationService]);
3524
3273
  }
3274
+
3525
3275
  function provideFullscreen() {
3526
3276
  return makeEnvironmentProviders([FullscreenService]);
3527
3277
  }
3528
- function provideFileSystemAccess() {
3529
- return makeEnvironmentProviders([PermissionsService, FileSystemAccessService]);
3530
- }
3531
- function provideMediaRecorder() {
3532
- return makeEnvironmentProviders([PermissionsService, MediaRecorderService]);
3533
- }
3278
+
3534
3279
  function provideServerSentEvents() {
3535
3280
  return makeEnvironmentProviders([ServerSentEventsService]);
3536
3281
  }
3282
+
3537
3283
  function provideVibration() {
3538
3284
  return makeEnvironmentProviders([VibrationService]);
3539
3285
  }
3286
+
3540
3287
  function provideSpeechSynthesis() {
3541
3288
  return makeEnvironmentProviders([SpeechSynthesisService]);
3542
3289
  }
3290
+
3543
3291
  function provideMutationObserver() {
3544
3292
  return makeEnvironmentProviders([MutationObserverService]);
3545
3293
  }
3294
+
3546
3295
  function providePerformanceObserver() {
3547
3296
  return makeEnvironmentProviders([PerformanceObserverService]);
3548
3297
  }
3549
- function provideIdleDetector() {
3550
- return makeEnvironmentProviders([PermissionsService, IdleDetectorService]);
3551
- }
3552
- function provideEyeDropper() {
3553
- return makeEnvironmentProviders([EyeDropperService]);
3554
- }
3555
- function provideBarcodeDetector() {
3556
- return makeEnvironmentProviders([BarcodeDetectorService]);
3557
- }
3298
+
3558
3299
  function provideWebAudio() {
3559
3300
  return makeEnvironmentProviders([WebAudioService]);
3560
3301
  }
3302
+
3561
3303
  function provideGamepad() {
3562
3304
  return makeEnvironmentProviders([GamepadService]);
3563
3305
  }
3564
- function provideWebBluetooth() {
3565
- return makeEnvironmentProviders([WebBluetoothService]);
3306
+
3307
+ function provideMediaApis() {
3308
+ return makeEnvironmentProviders([PermissionsService, CameraService, MediaDevicesService]);
3566
3309
  }
3567
- function provideWebUsb() {
3568
- return makeEnvironmentProviders([WebUsbService]);
3310
+ function provideLocationApis() {
3311
+ return makeEnvironmentProviders([PermissionsService, GeolocationService]);
3569
3312
  }
3570
- function provideWebNfc() {
3571
- return makeEnvironmentProviders([WebNfcService]);
3313
+ function provideStorageApis() {
3314
+ return makeEnvironmentProviders([PermissionsService, ClipboardService, WebStorageService]);
3572
3315
  }
3573
- function providePaymentRequest() {
3574
- return makeEnvironmentProviders([PaymentRequestService]);
3316
+ function provideCommunicationApis() {
3317
+ return makeEnvironmentProviders([
3318
+ PermissionsService,
3319
+ NotificationService,
3320
+ WebShareService,
3321
+ WebSocketService,
3322
+ ]);
3575
3323
  }
3576
- function provideCredentialManagement() {
3577
- return makeEnvironmentProviders([CredentialManagementService]);
3324
+
3325
+ const defaultBrowserWebApisConfig = {
3326
+ enableCamera: true,
3327
+ enableGeolocation: true,
3328
+ enableNotifications: true,
3329
+ enableClipboard: true,
3330
+ enableBattery: false,
3331
+ enableMediaDevices: true,
3332
+ enableWebShare: false,
3333
+ enableWebStorage: false,
3334
+ enableWebSocket: false,
3335
+ enableWebWorker: false,
3336
+ enableIntersectionObserver: false,
3337
+ enableResizeObserver: false,
3338
+ enablePageVisibility: false,
3339
+ enableBroadcastChannel: false,
3340
+ enableNetworkInformation: false,
3341
+ enableScreenWakeLock: false,
3342
+ enableScreenOrientation: false,
3343
+ enableFullscreen: false,
3344
+ enableFileSystemAccess: false,
3345
+ enableMediaRecorder: false,
3346
+ enableServerSentEvents: false,
3347
+ enableVibration: false,
3348
+ enableSpeechSynthesis: false,
3349
+ enableMutationObserver: false,
3350
+ enablePerformanceObserver: false,
3351
+ enableWebAudio: false,
3352
+ enableGamepad: false,
3353
+ };
3354
+ function provideBrowserWebApis(config = {}) {
3355
+ const mergedConfig = { ...defaultBrowserWebApisConfig, ...config };
3356
+ const providers = [PermissionsService];
3357
+ const conditionalProviders = [
3358
+ [mergedConfig.enableCamera, CameraService],
3359
+ [mergedConfig.enableGeolocation, GeolocationService],
3360
+ [mergedConfig.enableNotifications, NotificationService],
3361
+ [mergedConfig.enableClipboard, ClipboardService],
3362
+ [mergedConfig.enableMediaDevices, MediaDevicesService],
3363
+ [mergedConfig.enableBattery, BatteryService],
3364
+ [mergedConfig.enableWebShare, WebShareService],
3365
+ [mergedConfig.enableWebStorage, WebStorageService],
3366
+ [mergedConfig.enableWebSocket, WebSocketService],
3367
+ [mergedConfig.enableWebWorker, WebWorkerService],
3368
+ [mergedConfig.enableIntersectionObserver, IntersectionObserverService],
3369
+ [mergedConfig.enableResizeObserver, ResizeObserverService],
3370
+ [mergedConfig.enablePageVisibility, PageVisibilityService],
3371
+ [mergedConfig.enableBroadcastChannel, BroadcastChannelService],
3372
+ [mergedConfig.enableNetworkInformation, NetworkInformationService],
3373
+ [mergedConfig.enableScreenWakeLock, ScreenWakeLockService],
3374
+ [mergedConfig.enableScreenOrientation, ScreenOrientationService],
3375
+ [mergedConfig.enableFullscreen, FullscreenService],
3376
+ [mergedConfig.enableFileSystemAccess, FileSystemAccessService],
3377
+ [mergedConfig.enableMediaRecorder, MediaRecorderService],
3378
+ [mergedConfig.enableServerSentEvents, ServerSentEventsService],
3379
+ [mergedConfig.enableVibration, VibrationService],
3380
+ [mergedConfig.enableSpeechSynthesis, SpeechSynthesisService],
3381
+ [mergedConfig.enableMutationObserver, MutationObserverService],
3382
+ [mergedConfig.enablePerformanceObserver, PerformanceObserverService],
3383
+ [mergedConfig.enableWebAudio, WebAudioService],
3384
+ [mergedConfig.enableGamepad, GamepadService],
3385
+ ];
3386
+ for (const [enabled, provider] of conditionalProviders) {
3387
+ if (enabled) {
3388
+ providers.push(provider);
3389
+ }
3390
+ }
3391
+ return makeEnvironmentProviders(providers);
3578
3392
  }
3579
3393
 
3580
3394
  // Browser Web APIs Services
@@ -3585,4 +3399,4 @@ const version = '0.1.0';
3585
3399
  * Generated bundle index. Do not edit.
3586
3400
  */
3587
3401
 
3588
- export { BarcodeDetectorService, BatteryService, BroadcastChannelService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, ConnectionRegistryBaseService, CredentialManagementService, EyeDropperService, FileSystemAccessService, FullscreenService, GamepadService, GeolocationService, IdleDetectorService, IntersectionObserverService, MediaDevicesService, MediaRecorderService, MutationObserverService, NetworkInformationService, NotificationService, PageVisibilityService, PaymentRequestService, PerformanceObserverService, PermissionAwareBrowserApiBaseService, PermissionsService, ResizeObserverService, ScreenOrientationService, ScreenWakeLockService, ServerSentEventsService, SpeechSynthesisService, VibrationService, WebAudioService, WebBluetoothService, WebNfcService, WebShareService, WebSocketService, WebStorageService, WebUsbService, WebWorkerService, permissionGuard as createPermissionGuard, defaultBrowserWebApisConfig, injectGamepad, injectIdleDetector, injectIntersectionObserver, injectMutationObserver, injectNetworkInformation, injectPageVisibility, injectPerformanceObserver, injectResizeObserver, injectScreenOrientation, permissionGuard, provideBarcodeDetector, provideBattery, provideBroadcastChannel, provideBrowserWebApis, provideCamera, provideClipboard, provideCommunicationApis, provideCredentialManagement, provideEyeDropper, provideFileSystemAccess, provideFullscreen, provideGamepad, provideGeolocation, provideIdleDetector, provideIntersectionObserver, provideLocationApis, provideMediaApis, provideMediaDevices, provideMediaRecorder, provideMutationObserver, provideNetworkInformation, provideNotifications, providePageVisibility, providePaymentRequest, providePerformanceObserver, providePermissions, provideResizeObserver, provideScreenOrientation, provideScreenWakeLock, provideServerSentEvents, provideSpeechSynthesis, provideStorageApis, provideVibration, provideWebAudio, provideWebBluetooth, provideWebNfc, provideWebShare, provideWebSocket, provideWebStorage, provideWebUsb, provideWebWorker, version };
3402
+ export { BROWSER_API_LOGGER, BatteryService, BroadcastChannelService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, ConnectionRegistryBaseService, FileSystemAccessService, FullscreenService, GamepadService, GeolocationService, IntersectionObserverService, MediaDevicesService, MediaRecorderService, MutationObserverService, NetworkInformationService, NotificationService, PageVisibilityService, PerformanceObserverService, PermissionsService, ResizeObserverService, ScreenOrientationService, ScreenWakeLockService, ServerSentEventsService, SpeechSynthesisService, VibrationService, WebAudioService, WebShareService, WebSocketClient, WebSocketService, WebStorageService, WebWorkerService, permissionGuard as createPermissionGuard, defaultBrowserWebApisConfig, injectGamepad, injectIntersectionObserver, injectMutationObserver, injectNetworkInformation, injectPageVisibility, injectPerformanceObserver, injectResizeObserver, injectScreenOrientation, permissionGuard, provideBattery, provideBroadcastChannel, provideBrowserWebApis, provideCamera, provideClipboard, provideCommunicationApis, provideFileSystemAccess, provideFullscreen, provideGamepad, provideGeolocation, provideIntersectionObserver, provideLocationApis, provideMediaApis, provideMediaDevices, provideMediaRecorder, provideMutationObserver, provideNetworkInformation, provideNotifications, providePageVisibility, providePerformanceObserver, providePermissions, provideResizeObserver, provideScreenOrientation, provideScreenWakeLock, provideServerSentEvents, provideSpeechSynthesis, provideStorageApis, provideVibration, provideWebAudio, provideWebShare, provideWebSocket, provideWebStorage, provideWebWorker, version };