@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.
- package/README.es.md +68 -4
- package/README.md +64 -5
- package/fesm2022/angular-helpers-browser-web-apis-experimental.mjs +478 -0
- package/fesm2022/angular-helpers-browser-web-apis.mjs +739 -925
- package/package.json +6 -2
- package/types/angular-helpers-browser-web-apis-experimental.d.ts +325 -0
- package/types/angular-helpers-browser-web-apis.d.ts +253 -397
|
@@ -1,33 +1,22 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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:
|
|
61
|
-
*
|
|
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
|
|
69
|
+
* Log an error through the injected BROWSER_API_LOGGER (default: console).
|
|
80
70
|
*/
|
|
81
71
|
logError(message, error) {
|
|
82
|
-
|
|
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
|
|
81
|
+
* Log an informational message through the injected BROWSER_API_LOGGER (default: console).
|
|
86
82
|
*/
|
|
87
83
|
logInfo(message) {
|
|
88
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
117
|
-
return status.state === 'granted';
|
|
102
|
+
return await navigator.permissions.query(descriptor);
|
|
118
103
|
}
|
|
119
104
|
catch (error) {
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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:
|
|
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
|
|
125
|
+
class CameraService extends BrowserApiBaseService {
|
|
132
126
|
currentStream = null;
|
|
133
127
|
getApiName() {
|
|
134
128
|
return 'camera';
|
|
135
129
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
209
|
+
return capabilities ?? null;
|
|
214
210
|
}
|
|
215
211
|
catch (error) {
|
|
216
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
249
|
+
ensureSupported() {
|
|
250
|
+
super.ensureSupported();
|
|
254
251
|
if (!('geolocation' in navigator)) {
|
|
255
|
-
throw new Error('Geolocation API not supported
|
|
252
|
+
throw new Error('Geolocation API not supported — a secure context (HTTPS) is required');
|
|
256
253
|
}
|
|
257
254
|
}
|
|
258
255
|
getCurrentPosition(options) {
|
|
259
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
459
|
+
class ClipboardService extends BrowserApiBaseService {
|
|
460
460
|
getApiName() {
|
|
461
461
|
return 'clipboard';
|
|
462
462
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
470
|
+
this.ensureSupported();
|
|
476
471
|
try {
|
|
477
472
|
await navigator.clipboard.writeText(text);
|
|
478
473
|
}
|
|
479
474
|
catch (error) {
|
|
480
|
-
|
|
475
|
+
this.logError('Error writing to clipboard:', error);
|
|
481
476
|
throw error;
|
|
482
477
|
}
|
|
483
478
|
}
|
|
484
479
|
async readText() {
|
|
485
|
-
|
|
480
|
+
this.ensureSupported();
|
|
486
481
|
try {
|
|
487
482
|
return await navigator.clipboard.readText();
|
|
488
483
|
}
|
|
489
484
|
catch (error) {
|
|
490
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
749
|
+
this.ensureSupported();
|
|
753
750
|
try {
|
|
754
751
|
await navigator.share(data);
|
|
755
752
|
return { shared: true };
|
|
756
753
|
}
|
|
757
754
|
catch (error) {
|
|
758
|
-
|
|
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.
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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.
|
|
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
|
-
|
|
852
|
+
this.logError('Error setting localStorage:', error);
|
|
851
853
|
return false;
|
|
852
854
|
}
|
|
853
855
|
}
|
|
854
856
|
getLocalStorage(key, defaultValue = null, options = {}) {
|
|
855
|
-
this.
|
|
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
|
-
|
|
864
|
+
this.logError('Error getting localStorage:', error);
|
|
863
865
|
return defaultValue;
|
|
864
866
|
}
|
|
865
867
|
}
|
|
866
868
|
removeLocalStorage(key, options = {}) {
|
|
867
|
-
this.
|
|
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
|
-
|
|
879
|
+
this.logError('Error removing localStorage:', error);
|
|
875
880
|
return false;
|
|
876
881
|
}
|
|
877
882
|
}
|
|
878
883
|
clearLocalStorage(options = {}) {
|
|
879
|
-
this.
|
|
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) =>
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
925
|
+
this.logError('Error setting sessionStorage:', error);
|
|
914
926
|
return false;
|
|
915
927
|
}
|
|
916
928
|
}
|
|
917
929
|
getSessionStorage(key, defaultValue = null, options = {}) {
|
|
918
|
-
this.
|
|
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
|
-
|
|
937
|
+
this.logError('Error getting sessionStorage:', error);
|
|
926
938
|
return defaultValue;
|
|
927
939
|
}
|
|
928
940
|
}
|
|
929
941
|
removeSessionStorage(key, options = {}) {
|
|
930
|
-
this.
|
|
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
|
-
|
|
952
|
+
this.logError('Error removing sessionStorage:', error);
|
|
938
953
|
return false;
|
|
939
954
|
}
|
|
940
955
|
}
|
|
941
956
|
clearSessionStorage(options = {}) {
|
|
942
|
-
this.
|
|
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) =>
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
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.
|
|
1025
|
+
this.ensureSupported();
|
|
1017
1026
|
return localStorage;
|
|
1018
1027
|
}
|
|
1019
1028
|
getNativeSessionStorage() {
|
|
1020
|
-
this.
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
|
|
1046
|
-
this.
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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.
|
|
1090
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
1098
1091
|
throw new Error('WebSocket is not connected');
|
|
1099
1092
|
}
|
|
1100
|
-
const
|
|
1093
|
+
const enriched = {
|
|
1101
1094
|
...message,
|
|
1102
|
-
|
|
1095
|
+
id: message.id ?? this.generateId(),
|
|
1096
|
+
timestamp: message.timestamp ?? Date.now(),
|
|
1103
1097
|
};
|
|
1104
|
-
this.
|
|
1098
|
+
this.socket.send(JSON.stringify(enriched));
|
|
1105
1099
|
}
|
|
1106
1100
|
sendRaw(data) {
|
|
1107
|
-
if (!this.
|
|
1101
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
1108
1102
|
throw new Error('WebSocket is not connected');
|
|
1109
1103
|
}
|
|
1110
|
-
this.
|
|
1104
|
+
this.socket.send(data);
|
|
1111
1105
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
1121
|
-
if (!this.
|
|
1170
|
+
attachHandlers() {
|
|
1171
|
+
if (!this.socket)
|
|
1122
1172
|
return;
|
|
1123
|
-
this.
|
|
1124
|
-
console.log('[WebSocketService] Connected to:', config.url);
|
|
1173
|
+
this.socket.onopen = () => {
|
|
1125
1174
|
this.reconnectAttempts = 0;
|
|
1126
|
-
this.updateStatus({
|
|
1127
|
-
|
|
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.
|
|
1138
|
-
|
|
1178
|
+
this.socket.onclose = (event) => {
|
|
1179
|
+
this.stopHeartbeat();
|
|
1180
|
+
if (this.disposed)
|
|
1181
|
+
return;
|
|
1139
1182
|
this.updateStatus({
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
reconnecting: false,
|
|
1143
|
-
reconnectAttempts: this.reconnectAttempts,
|
|
1183
|
+
state: 'closed',
|
|
1184
|
+
error: event.wasClean ? null : `closed: ${event.code} ${event.reason}`,
|
|
1144
1185
|
});
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
this.attemptReconnect(config);
|
|
1186
|
+
if (!event.wasClean) {
|
|
1187
|
+
this.scheduleReconnect();
|
|
1148
1188
|
}
|
|
1149
1189
|
};
|
|
1150
|
-
this.
|
|
1151
|
-
|
|
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.
|
|
1161
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1181
|
-
if (this.
|
|
1182
|
-
|
|
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
|
-
|
|
1188
|
-
connecting: false,
|
|
1189
|
-
reconnecting: true,
|
|
1235
|
+
state: 'reconnecting',
|
|
1190
1236
|
reconnectAttempts: this.reconnectAttempts,
|
|
1191
1237
|
});
|
|
1192
1238
|
this.reconnectTimer = setTimeout(() => {
|
|
1193
|
-
|
|
1194
|
-
this.
|
|
1195
|
-
},
|
|
1239
|
+
this.reconnectTimer = null;
|
|
1240
|
+
this.openSocket();
|
|
1241
|
+
}, delay);
|
|
1196
1242
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1269
|
+
stopHeartbeat() {
|
|
1270
|
+
if (this.heartbeatTimer) {
|
|
1271
|
+
clearInterval(this.heartbeatTimer);
|
|
1272
|
+
this.heartbeatTimer = null;
|
|
1273
|
+
}
|
|
1213
1274
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1275
|
+
clearTimers() {
|
|
1276
|
+
if (this.reconnectTimer) {
|
|
1277
|
+
clearTimeout(this.reconnectTimer);
|
|
1278
|
+
this.reconnectTimer = null;
|
|
1279
|
+
}
|
|
1280
|
+
this.stopHeartbeat();
|
|
1216
1281
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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 '
|
|
1322
|
+
return 'websocket';
|
|
1235
1323
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
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.
|
|
1245
|
-
|
|
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
|
|
1413
|
+
// No-op
|
|
1248
1414
|
};
|
|
1249
1415
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1965
|
-
|
|
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.
|
|
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
|
-
|
|
2243
|
+
this.logError('Error opening file:', error);
|
|
1986
2244
|
throw error;
|
|
1987
2245
|
}
|
|
1988
2246
|
}
|
|
1989
2247
|
async saveFile(content, options = {}) {
|
|
1990
|
-
this.
|
|
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
|
-
|
|
2262
|
+
this.logError('Error saving file:', error);
|
|
2005
2263
|
throw error;
|
|
2006
2264
|
}
|
|
2007
2265
|
}
|
|
2008
2266
|
async openDirectory(options = {}) {
|
|
2009
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3369
|
-
|
|
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
|
-
|
|
3198
|
+
|
|
3453
3199
|
function provideCamera() {
|
|
3454
|
-
return makeEnvironmentProviders([
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3565
|
-
|
|
3306
|
+
|
|
3307
|
+
function provideMediaApis() {
|
|
3308
|
+
return makeEnvironmentProviders([PermissionsService, CameraService, MediaDevicesService]);
|
|
3566
3309
|
}
|
|
3567
|
-
function
|
|
3568
|
-
return makeEnvironmentProviders([
|
|
3310
|
+
function provideLocationApis() {
|
|
3311
|
+
return makeEnvironmentProviders([PermissionsService, GeolocationService]);
|
|
3569
3312
|
}
|
|
3570
|
-
function
|
|
3571
|
-
return makeEnvironmentProviders([
|
|
3313
|
+
function provideStorageApis() {
|
|
3314
|
+
return makeEnvironmentProviders([PermissionsService, ClipboardService, WebStorageService]);
|
|
3572
3315
|
}
|
|
3573
|
-
function
|
|
3574
|
-
return makeEnvironmentProviders([
|
|
3316
|
+
function provideCommunicationApis() {
|
|
3317
|
+
return makeEnvironmentProviders([
|
|
3318
|
+
PermissionsService,
|
|
3319
|
+
NotificationService,
|
|
3320
|
+
WebShareService,
|
|
3321
|
+
WebSocketService,
|
|
3322
|
+
]);
|
|
3575
3323
|
}
|
|
3576
|
-
|
|
3577
|
-
|
|
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 {
|
|
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 };
|