@angular-helpers/browser-web-apis 21.7.0 → 21.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, DestroyRef, PLATFORM_ID,
|
|
2
|
+
import { InjectionToken, Injectable, inject, DestroyRef, PLATFORM_ID, signal, computed, isSignal, effect, ElementRef, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
|
4
4
|
import { Observable, fromEvent, Subject, of, map as map$1 } from 'rxjs';
|
|
5
5
|
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
@@ -18,14 +18,180 @@ const BROWSER_API_LOGGER = new InjectionToken('BROWSER_API_LOGGER', {
|
|
|
18
18
|
}),
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
const BROWSER_CAPABILITIES = [
|
|
22
|
+
{ id: 'permissions', label: 'Permissions API', requiresSecureContext: false },
|
|
23
|
+
{ id: 'geolocation', label: 'Geolocation API', requiresSecureContext: true },
|
|
24
|
+
{ id: 'clipboard', label: 'Clipboard API', requiresSecureContext: true },
|
|
25
|
+
{ id: 'notification', label: 'Notification API', requiresSecureContext: true },
|
|
26
|
+
{ id: 'mediaDevices', label: 'MediaDevices API', requiresSecureContext: true },
|
|
27
|
+
{ id: 'camera', label: 'Camera API', requiresSecureContext: true },
|
|
28
|
+
{ id: 'webWorker', label: 'Web Worker API', requiresSecureContext: false },
|
|
29
|
+
{ id: 'regexSecurity', label: 'Regex Security', requiresSecureContext: false },
|
|
30
|
+
{ id: 'webStorage', label: 'Web Storage', requiresSecureContext: false },
|
|
31
|
+
{ id: 'webShare', label: 'Web Share', requiresSecureContext: true },
|
|
32
|
+
{ id: 'battery', label: 'Battery API', requiresSecureContext: false },
|
|
33
|
+
{ id: 'webSocket', label: 'WebSocket API', requiresSecureContext: false },
|
|
34
|
+
{ id: 'intersectionObserver', label: 'Intersection Observer', requiresSecureContext: false },
|
|
35
|
+
{ id: 'resizeObserver', label: 'Resize Observer', requiresSecureContext: false },
|
|
36
|
+
{ id: 'pageVisibility', label: 'Page Visibility API', requiresSecureContext: false },
|
|
37
|
+
{ id: 'broadcastChannel', label: 'Broadcast Channel API', requiresSecureContext: false },
|
|
38
|
+
{ id: 'networkInformation', label: 'Network Information API', requiresSecureContext: false },
|
|
39
|
+
{ id: 'screenWakeLock', label: 'Screen Wake Lock API', requiresSecureContext: true },
|
|
40
|
+
{ id: 'screenOrientation', label: 'Screen Orientation API', requiresSecureContext: false },
|
|
41
|
+
{ id: 'fullscreen', label: 'Fullscreen API', requiresSecureContext: false },
|
|
42
|
+
{ id: 'fileSystemAccess', label: 'File System Access API', requiresSecureContext: true },
|
|
43
|
+
{ id: 'mediaRecorder', label: 'MediaRecorder API', requiresSecureContext: true },
|
|
44
|
+
{ id: 'serverSentEvents', label: 'Server-Sent Events', requiresSecureContext: false },
|
|
45
|
+
{ id: 'vibration', label: 'Vibration API', requiresSecureContext: false },
|
|
46
|
+
{ id: 'speechSynthesis', label: 'Speech Synthesis API', requiresSecureContext: false },
|
|
47
|
+
{ id: 'mutationObserver', label: 'Mutation Observer', requiresSecureContext: false },
|
|
48
|
+
{ id: 'performanceObserver', label: 'Performance Observer', requiresSecureContext: false },
|
|
49
|
+
{ id: 'idleDetector', label: 'Idle Detection API', requiresSecureContext: true },
|
|
50
|
+
{ id: 'eyeDropper', label: 'EyeDropper API', requiresSecureContext: true },
|
|
51
|
+
{ id: 'barcodeDetector', label: 'Barcode Detection API', requiresSecureContext: true },
|
|
52
|
+
{ id: 'webAudio', label: 'Web Audio API', requiresSecureContext: false },
|
|
53
|
+
{ id: 'gamepad', label: 'Gamepad API', requiresSecureContext: true },
|
|
54
|
+
{ id: 'webBluetooth', label: 'Web Bluetooth API', requiresSecureContext: true },
|
|
55
|
+
{ id: 'webUsb', label: 'WebUSB API', requiresSecureContext: true },
|
|
56
|
+
{ id: 'webNfc', label: 'Web NFC API', requiresSecureContext: true },
|
|
57
|
+
{ id: 'paymentRequest', label: 'Payment Request API', requiresSecureContext: true },
|
|
58
|
+
{ id: 'credentialManagement', label: 'Credential Management API', requiresSecureContext: true },
|
|
59
|
+
];
|
|
60
|
+
class BrowserCapabilityService {
|
|
61
|
+
getCapabilities() {
|
|
62
|
+
return BROWSER_CAPABILITIES;
|
|
63
|
+
}
|
|
64
|
+
isSecureContext() {
|
|
65
|
+
return typeof window !== 'undefined' && window.isSecureContext;
|
|
66
|
+
}
|
|
67
|
+
isSupported(capability) {
|
|
68
|
+
switch (capability) {
|
|
69
|
+
case 'permissions':
|
|
70
|
+
return typeof navigator !== 'undefined' && 'permissions' in navigator;
|
|
71
|
+
case 'geolocation':
|
|
72
|
+
return typeof navigator !== 'undefined' && 'geolocation' in navigator;
|
|
73
|
+
case 'clipboard':
|
|
74
|
+
return typeof navigator !== 'undefined' && 'clipboard' in navigator;
|
|
75
|
+
case 'notification':
|
|
76
|
+
return typeof window !== 'undefined' && 'Notification' in window;
|
|
77
|
+
case 'mediaDevices':
|
|
78
|
+
case 'camera':
|
|
79
|
+
return typeof navigator !== 'undefined' && 'mediaDevices' in navigator;
|
|
80
|
+
case 'webWorker':
|
|
81
|
+
case 'regexSecurity':
|
|
82
|
+
return typeof Worker !== 'undefined';
|
|
83
|
+
case 'webStorage':
|
|
84
|
+
return typeof Storage !== 'undefined';
|
|
85
|
+
case 'webShare':
|
|
86
|
+
return typeof navigator !== 'undefined' && 'share' in navigator;
|
|
87
|
+
case 'battery':
|
|
88
|
+
return typeof navigator !== 'undefined' && 'getBattery' in navigator;
|
|
89
|
+
case 'webSocket':
|
|
90
|
+
return typeof WebSocket !== 'undefined';
|
|
91
|
+
case 'intersectionObserver':
|
|
92
|
+
return typeof IntersectionObserver !== 'undefined';
|
|
93
|
+
case 'resizeObserver':
|
|
94
|
+
return typeof ResizeObserver !== 'undefined';
|
|
95
|
+
case 'pageVisibility':
|
|
96
|
+
return typeof document !== 'undefined' && 'hidden' in document;
|
|
97
|
+
case 'broadcastChannel':
|
|
98
|
+
return typeof BroadcastChannel !== 'undefined';
|
|
99
|
+
case 'networkInformation':
|
|
100
|
+
return (typeof navigator !== 'undefined' &&
|
|
101
|
+
('connection' in navigator || 'mozConnection' in navigator));
|
|
102
|
+
case 'screenWakeLock':
|
|
103
|
+
return typeof navigator !== 'undefined' && 'wakeLock' in navigator;
|
|
104
|
+
case 'screenOrientation':
|
|
105
|
+
return typeof screen !== 'undefined' && 'orientation' in screen;
|
|
106
|
+
case 'fullscreen':
|
|
107
|
+
return (typeof document !== 'undefined' &&
|
|
108
|
+
('fullscreenEnabled' in document || 'webkitFullscreenEnabled' in document));
|
|
109
|
+
case 'fileSystemAccess':
|
|
110
|
+
return typeof window !== 'undefined' && 'showOpenFilePicker' in window;
|
|
111
|
+
case 'mediaRecorder':
|
|
112
|
+
return typeof MediaRecorder !== 'undefined';
|
|
113
|
+
case 'serverSentEvents':
|
|
114
|
+
return typeof EventSource !== 'undefined';
|
|
115
|
+
case 'vibration':
|
|
116
|
+
return typeof navigator !== 'undefined' && 'vibrate' in navigator;
|
|
117
|
+
case 'speechSynthesis':
|
|
118
|
+
return typeof window !== 'undefined' && 'speechSynthesis' in window;
|
|
119
|
+
case 'mutationObserver':
|
|
120
|
+
return typeof MutationObserver !== 'undefined';
|
|
121
|
+
case 'performanceObserver':
|
|
122
|
+
return typeof PerformanceObserver !== 'undefined';
|
|
123
|
+
case 'idleDetector':
|
|
124
|
+
return typeof window !== 'undefined' && 'IdleDetector' in window;
|
|
125
|
+
case 'eyeDropper':
|
|
126
|
+
return typeof window !== 'undefined' && 'EyeDropper' in window;
|
|
127
|
+
case 'barcodeDetector':
|
|
128
|
+
return typeof window !== 'undefined' && 'BarcodeDetector' in window;
|
|
129
|
+
case 'webAudio':
|
|
130
|
+
return typeof window !== 'undefined' && 'AudioContext' in window;
|
|
131
|
+
case 'gamepad':
|
|
132
|
+
return typeof navigator !== 'undefined' && 'getGamepads' in navigator;
|
|
133
|
+
case 'webBluetooth':
|
|
134
|
+
return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
|
|
135
|
+
case 'webUsb':
|
|
136
|
+
return typeof navigator !== 'undefined' && 'usb' in navigator;
|
|
137
|
+
case 'webNfc':
|
|
138
|
+
return typeof window !== 'undefined' && 'NDEFReader' in window;
|
|
139
|
+
case 'paymentRequest':
|
|
140
|
+
return typeof window !== 'undefined' && 'PaymentRequest' in window;
|
|
141
|
+
case 'credentialManagement':
|
|
142
|
+
return typeof navigator !== 'undefined' && 'credentials' in navigator;
|
|
143
|
+
default:
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
getAllStatuses() {
|
|
148
|
+
const secureContext = this.isSecureContext();
|
|
149
|
+
return this.getCapabilities().map((capability) => ({
|
|
150
|
+
id: capability.id,
|
|
151
|
+
label: capability.label,
|
|
152
|
+
supported: this.isSupported(capability.id),
|
|
153
|
+
secureContext,
|
|
154
|
+
requiresSecureContext: capability.requiresSecureContext,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
async getPermissionState(permission) {
|
|
158
|
+
if (typeof navigator === 'undefined' || !('permissions' in navigator)) {
|
|
159
|
+
return 'unknown';
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const status = await navigator.permissions.query({ name: permission });
|
|
163
|
+
return status.state;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return 'unknown';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
170
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, providedIn: 'root' });
|
|
171
|
+
}
|
|
172
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, decorators: [{
|
|
173
|
+
type: Injectable,
|
|
174
|
+
args: [{ providedIn: 'root' }]
|
|
175
|
+
}] });
|
|
176
|
+
|
|
21
177
|
/**
|
|
22
178
|
* Base class for all Browser Web API services.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* -
|
|
179
|
+
*
|
|
180
|
+
* ## Support detection contract
|
|
181
|
+
*
|
|
182
|
+
* Services follow ONE pattern (do not invent variants):
|
|
183
|
+
*
|
|
184
|
+
* - `isSupported(): boolean` — public, side-effect free, SSR-safe. Default
|
|
185
|
+
* implementation delegates to {@link BrowserCapabilityService} when the subclass
|
|
186
|
+
* overrides {@link getCapabilityId} (recommended).
|
|
187
|
+
* - `ensureSupported(): void` — internal Template Method. Throws when called outside
|
|
188
|
+
* browser or when the underlying API is missing.
|
|
189
|
+
*
|
|
190
|
+
* ## Error surfacing contract
|
|
191
|
+
*
|
|
192
|
+
* - **Imperative methods** MUST call `ensureSupported()` and throw synchronously.
|
|
193
|
+
* - **Stream-returning methods** MUST guard with `isSupported()` and surface
|
|
194
|
+
* unsupported state as `Observable.error(...)` (NOT throw inline).
|
|
29
195
|
*
|
|
30
196
|
* Services that also need permission querying should extend
|
|
31
197
|
* `PermissionAwareBrowserApiBaseService` instead.
|
|
@@ -34,30 +200,46 @@ class BrowserApiBaseService {
|
|
|
34
200
|
destroyRef = inject(DestroyRef);
|
|
35
201
|
platformId = inject(PLATFORM_ID);
|
|
36
202
|
logger = inject(BROWSER_API_LOGGER);
|
|
203
|
+
capabilities = inject(BrowserCapabilityService);
|
|
37
204
|
/**
|
|
38
|
-
*
|
|
205
|
+
* Optional hook for subclasses to delegate feature detection to
|
|
206
|
+
* {@link BrowserCapabilityService}. Returning a capability id removes the need to
|
|
207
|
+
* implement `isSupported()` manually and avoids drift between per-service checks
|
|
208
|
+
* and the centralized capability registry.
|
|
39
209
|
*/
|
|
210
|
+
getCapabilityId() {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
/** Public, SSR-safe support check. Override only if you need extra constraints. */
|
|
214
|
+
isSupported() {
|
|
215
|
+
if (!this.isBrowserEnvironment())
|
|
216
|
+
return false;
|
|
217
|
+
const capabilityId = this.getCapabilityId();
|
|
218
|
+
if (capabilityId !== null) {
|
|
219
|
+
return this.capabilities.isSupported(capabilityId);
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
40
223
|
isBrowserEnvironment() {
|
|
41
224
|
return isPlatformBrowser(this.platformId);
|
|
42
225
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Check if running in server environment using Angular's platform detection.
|
|
45
|
-
*/
|
|
46
226
|
isServerEnvironment() {
|
|
47
227
|
return isPlatformServer(this.platformId);
|
|
48
228
|
}
|
|
49
229
|
/**
|
|
50
|
-
* Template Method: asserts the service can run in the current environment.
|
|
51
|
-
*
|
|
230
|
+
* Template Method: asserts the service can run in the current environment. Subclasses
|
|
231
|
+
* that need extra checks beyond capability detection MUST call `super.ensureSupported()`
|
|
232
|
+
* first, then add their own check.
|
|
52
233
|
*/
|
|
53
234
|
ensureSupported() {
|
|
54
235
|
if (!this.isBrowserEnvironment()) {
|
|
55
236
|
throw new Error(`${this.getApiName()} API not available in server environment`);
|
|
56
237
|
}
|
|
238
|
+
const capabilityId = this.getCapabilityId();
|
|
239
|
+
if (capabilityId !== null && !this.capabilities.isSupported(capabilityId)) {
|
|
240
|
+
throw new Error(`${this.getApiName()} API not supported in this browser`);
|
|
241
|
+
}
|
|
57
242
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Create an error with proper cause chaining.
|
|
60
|
-
*/
|
|
61
243
|
createError(message, cause) {
|
|
62
244
|
const error = new Error(message);
|
|
63
245
|
if (cause !== undefined) {
|
|
@@ -65,21 +247,12 @@ class BrowserApiBaseService {
|
|
|
65
247
|
}
|
|
66
248
|
return error;
|
|
67
249
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Log an error through the injected BROWSER_API_LOGGER (default: console).
|
|
70
|
-
*/
|
|
71
250
|
logError(message, error) {
|
|
72
251
|
this.logger.error(`[${this.getApiName()}] ${message}`, error);
|
|
73
252
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Log a warning through the injected BROWSER_API_LOGGER (default: console).
|
|
76
|
-
*/
|
|
77
253
|
logWarn(message) {
|
|
78
254
|
this.logger.warn(`[${this.getApiName()}] ${message}`);
|
|
79
255
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Log an informational message through the injected BROWSER_API_LOGGER (default: console).
|
|
82
|
-
*/
|
|
83
256
|
logInfo(message) {
|
|
84
257
|
this.logger.info(`[${this.getApiName()}] ${message}`);
|
|
85
258
|
}
|
|
@@ -112,8 +285,8 @@ class PermissionsService extends BrowserApiBaseService {
|
|
|
112
285
|
throw error;
|
|
113
286
|
}
|
|
114
287
|
}
|
|
115
|
-
|
|
116
|
-
return
|
|
288
|
+
getCapabilityId() {
|
|
289
|
+
return 'permissions';
|
|
117
290
|
}
|
|
118
291
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
119
292
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PermissionsService });
|
|
@@ -127,10 +300,13 @@ class CameraService extends BrowserApiBaseService {
|
|
|
127
300
|
getApiName() {
|
|
128
301
|
return 'camera';
|
|
129
302
|
}
|
|
303
|
+
getCapabilityId() {
|
|
304
|
+
return 'camera';
|
|
305
|
+
}
|
|
130
306
|
ensureSupported() {
|
|
131
307
|
super.ensureSupported();
|
|
132
308
|
if (!navigator.mediaDevices?.getUserMedia) {
|
|
133
|
-
throw new Error('Camera API not supported —
|
|
309
|
+
throw new Error('Camera API not supported — getUserMedia missing or HTTPS required');
|
|
134
310
|
}
|
|
135
311
|
}
|
|
136
312
|
async startCamera(constraints) {
|
|
@@ -246,11 +422,8 @@ class GeolocationService extends BrowserApiBaseService {
|
|
|
246
422
|
getApiName() {
|
|
247
423
|
return 'geolocation';
|
|
248
424
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (!('geolocation' in navigator)) {
|
|
252
|
-
throw new Error('Geolocation API not supported — a secure context (HTTPS) is required');
|
|
253
|
-
}
|
|
425
|
+
getCapabilityId() {
|
|
426
|
+
return 'geolocation';
|
|
254
427
|
}
|
|
255
428
|
getCurrentPosition(options) {
|
|
256
429
|
this.ensureSupported();
|
|
@@ -292,6 +465,9 @@ class MediaDevicesService extends BrowserApiBaseService {
|
|
|
292
465
|
getApiName() {
|
|
293
466
|
return 'media-devices';
|
|
294
467
|
}
|
|
468
|
+
getCapabilityId() {
|
|
469
|
+
return 'mediaDevices';
|
|
470
|
+
}
|
|
295
471
|
ensureSupported() {
|
|
296
472
|
super.ensureSupported();
|
|
297
473
|
if (!navigator.mediaDevices) {
|
|
@@ -420,11 +596,8 @@ class NotificationService extends BrowserApiBaseService {
|
|
|
420
596
|
getApiName() {
|
|
421
597
|
return 'notifications';
|
|
422
598
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (!('Notification' in window)) {
|
|
426
|
-
throw new Error('Notifications API not supported in this browser');
|
|
427
|
-
}
|
|
599
|
+
getCapabilityId() {
|
|
600
|
+
return 'notification';
|
|
428
601
|
}
|
|
429
602
|
get permission() {
|
|
430
603
|
if (!this.isBrowserEnvironment() || !('Notification' in window)) {
|
|
@@ -460,193 +633,40 @@ class ClipboardService extends BrowserApiBaseService {
|
|
|
460
633
|
getApiName() {
|
|
461
634
|
return 'clipboard';
|
|
462
635
|
}
|
|
636
|
+
getCapabilityId() {
|
|
637
|
+
return 'clipboard';
|
|
638
|
+
}
|
|
463
639
|
ensureSupported() {
|
|
464
640
|
super.ensureSupported();
|
|
465
641
|
if (!navigator.clipboard) {
|
|
466
642
|
throw new Error('Clipboard API not supported \u2014 a secure context (HTTPS) is required');
|
|
467
643
|
}
|
|
468
|
-
}
|
|
469
|
-
async writeText(text) {
|
|
470
|
-
this.ensureSupported();
|
|
471
|
-
try {
|
|
472
|
-
await navigator.clipboard.writeText(text);
|
|
473
|
-
}
|
|
474
|
-
catch (error) {
|
|
475
|
-
this.logError('Error writing to clipboard:', error);
|
|
476
|
-
throw error;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
async readText() {
|
|
480
|
-
this.ensureSupported();
|
|
481
|
-
try {
|
|
482
|
-
return await navigator.clipboard.readText();
|
|
483
|
-
}
|
|
484
|
-
catch (error) {
|
|
485
|
-
this.logError('Error reading from clipboard:', error);
|
|
486
|
-
throw error;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
490
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService });
|
|
491
|
-
}
|
|
492
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, decorators: [{
|
|
493
|
-
type: Injectable
|
|
494
|
-
}] });
|
|
495
|
-
|
|
496
|
-
const BROWSER_CAPABILITIES = [
|
|
497
|
-
{ id: 'permissions', label: 'Permissions API', requiresSecureContext: false },
|
|
498
|
-
{ id: 'geolocation', label: 'Geolocation API', requiresSecureContext: true },
|
|
499
|
-
{ id: 'clipboard', label: 'Clipboard API', requiresSecureContext: true },
|
|
500
|
-
{ id: 'notification', label: 'Notification API', requiresSecureContext: true },
|
|
501
|
-
{ id: 'mediaDevices', label: 'MediaDevices API', requiresSecureContext: true },
|
|
502
|
-
{ id: 'camera', label: 'Camera API', requiresSecureContext: true },
|
|
503
|
-
{ id: 'webWorker', label: 'Web Worker API', requiresSecureContext: false },
|
|
504
|
-
{ id: 'regexSecurity', label: 'Regex Security', requiresSecureContext: false },
|
|
505
|
-
{ id: 'webStorage', label: 'Web Storage', requiresSecureContext: false },
|
|
506
|
-
{ id: 'webShare', label: 'Web Share', requiresSecureContext: true },
|
|
507
|
-
{ id: 'battery', label: 'Battery API', requiresSecureContext: false },
|
|
508
|
-
{ id: 'webSocket', label: 'WebSocket API', requiresSecureContext: false },
|
|
509
|
-
{ id: 'intersectionObserver', label: 'Intersection Observer', requiresSecureContext: false },
|
|
510
|
-
{ id: 'resizeObserver', label: 'Resize Observer', requiresSecureContext: false },
|
|
511
|
-
{ id: 'pageVisibility', label: 'Page Visibility API', requiresSecureContext: false },
|
|
512
|
-
{ id: 'broadcastChannel', label: 'Broadcast Channel API', requiresSecureContext: false },
|
|
513
|
-
{ id: 'networkInformation', label: 'Network Information API', requiresSecureContext: false },
|
|
514
|
-
{ id: 'screenWakeLock', label: 'Screen Wake Lock API', requiresSecureContext: true },
|
|
515
|
-
{ id: 'screenOrientation', label: 'Screen Orientation API', requiresSecureContext: false },
|
|
516
|
-
{ id: 'fullscreen', label: 'Fullscreen API', requiresSecureContext: false },
|
|
517
|
-
{ id: 'fileSystemAccess', label: 'File System Access API', requiresSecureContext: true },
|
|
518
|
-
{ id: 'mediaRecorder', label: 'MediaRecorder API', requiresSecureContext: true },
|
|
519
|
-
{ id: 'serverSentEvents', label: 'Server-Sent Events', requiresSecureContext: false },
|
|
520
|
-
{ id: 'vibration', label: 'Vibration API', requiresSecureContext: false },
|
|
521
|
-
{ id: 'speechSynthesis', label: 'Speech Synthesis API', requiresSecureContext: false },
|
|
522
|
-
{ id: 'mutationObserver', label: 'Mutation Observer', requiresSecureContext: false },
|
|
523
|
-
{ id: 'performanceObserver', label: 'Performance Observer', requiresSecureContext: false },
|
|
524
|
-
{ id: 'idleDetector', label: 'Idle Detection API', requiresSecureContext: true },
|
|
525
|
-
{ id: 'eyeDropper', label: 'EyeDropper API', requiresSecureContext: true },
|
|
526
|
-
{ id: 'barcodeDetector', label: 'Barcode Detection API', requiresSecureContext: true },
|
|
527
|
-
{ id: 'webAudio', label: 'Web Audio API', requiresSecureContext: false },
|
|
528
|
-
{ id: 'gamepad', label: 'Gamepad API', requiresSecureContext: true },
|
|
529
|
-
{ id: 'webBluetooth', label: 'Web Bluetooth API', requiresSecureContext: true },
|
|
530
|
-
{ id: 'webUsb', label: 'WebUSB API', requiresSecureContext: true },
|
|
531
|
-
{ id: 'webNfc', label: 'Web NFC API', requiresSecureContext: true },
|
|
532
|
-
{ id: 'paymentRequest', label: 'Payment Request API', requiresSecureContext: true },
|
|
533
|
-
{ id: 'credentialManagement', label: 'Credential Management API', requiresSecureContext: true },
|
|
534
|
-
];
|
|
535
|
-
class BrowserCapabilityService {
|
|
536
|
-
getCapabilities() {
|
|
537
|
-
return BROWSER_CAPABILITIES;
|
|
538
|
-
}
|
|
539
|
-
isSecureContext() {
|
|
540
|
-
return typeof window !== 'undefined' && window.isSecureContext;
|
|
541
|
-
}
|
|
542
|
-
isSupported(capability) {
|
|
543
|
-
switch (capability) {
|
|
544
|
-
case 'permissions':
|
|
545
|
-
return typeof navigator !== 'undefined' && 'permissions' in navigator;
|
|
546
|
-
case 'geolocation':
|
|
547
|
-
return typeof navigator !== 'undefined' && 'geolocation' in navigator;
|
|
548
|
-
case 'clipboard':
|
|
549
|
-
return typeof navigator !== 'undefined' && 'clipboard' in navigator;
|
|
550
|
-
case 'notification':
|
|
551
|
-
return typeof window !== 'undefined' && 'Notification' in window;
|
|
552
|
-
case 'mediaDevices':
|
|
553
|
-
case 'camera':
|
|
554
|
-
return typeof navigator !== 'undefined' && 'mediaDevices' in navigator;
|
|
555
|
-
case 'webWorker':
|
|
556
|
-
case 'regexSecurity':
|
|
557
|
-
return typeof Worker !== 'undefined';
|
|
558
|
-
case 'webStorage':
|
|
559
|
-
return typeof Storage !== 'undefined';
|
|
560
|
-
case 'webShare':
|
|
561
|
-
return typeof navigator !== 'undefined' && 'share' in navigator;
|
|
562
|
-
case 'battery':
|
|
563
|
-
return typeof navigator !== 'undefined' && 'getBattery' in navigator;
|
|
564
|
-
case 'webSocket':
|
|
565
|
-
return typeof WebSocket !== 'undefined';
|
|
566
|
-
case 'intersectionObserver':
|
|
567
|
-
return typeof IntersectionObserver !== 'undefined';
|
|
568
|
-
case 'resizeObserver':
|
|
569
|
-
return typeof ResizeObserver !== 'undefined';
|
|
570
|
-
case 'pageVisibility':
|
|
571
|
-
return typeof document !== 'undefined' && 'hidden' in document;
|
|
572
|
-
case 'broadcastChannel':
|
|
573
|
-
return typeof BroadcastChannel !== 'undefined';
|
|
574
|
-
case 'networkInformation':
|
|
575
|
-
return (typeof navigator !== 'undefined' &&
|
|
576
|
-
('connection' in navigator || 'mozConnection' in navigator));
|
|
577
|
-
case 'screenWakeLock':
|
|
578
|
-
return typeof navigator !== 'undefined' && 'wakeLock' in navigator;
|
|
579
|
-
case 'screenOrientation':
|
|
580
|
-
return typeof screen !== 'undefined' && 'orientation' in screen;
|
|
581
|
-
case 'fullscreen':
|
|
582
|
-
return (typeof document !== 'undefined' &&
|
|
583
|
-
('fullscreenEnabled' in document || 'webkitFullscreenEnabled' in document));
|
|
584
|
-
case 'fileSystemAccess':
|
|
585
|
-
return typeof window !== 'undefined' && 'showOpenFilePicker' in window;
|
|
586
|
-
case 'mediaRecorder':
|
|
587
|
-
return typeof MediaRecorder !== 'undefined';
|
|
588
|
-
case 'serverSentEvents':
|
|
589
|
-
return typeof EventSource !== 'undefined';
|
|
590
|
-
case 'vibration':
|
|
591
|
-
return typeof navigator !== 'undefined' && 'vibrate' in navigator;
|
|
592
|
-
case 'speechSynthesis':
|
|
593
|
-
return typeof window !== 'undefined' && 'speechSynthesis' in window;
|
|
594
|
-
case 'mutationObserver':
|
|
595
|
-
return typeof MutationObserver !== 'undefined';
|
|
596
|
-
case 'performanceObserver':
|
|
597
|
-
return typeof PerformanceObserver !== 'undefined';
|
|
598
|
-
case 'idleDetector':
|
|
599
|
-
return typeof window !== 'undefined' && 'IdleDetector' in window;
|
|
600
|
-
case 'eyeDropper':
|
|
601
|
-
return typeof window !== 'undefined' && 'EyeDropper' in window;
|
|
602
|
-
case 'barcodeDetector':
|
|
603
|
-
return typeof window !== 'undefined' && 'BarcodeDetector' in window;
|
|
604
|
-
case 'webAudio':
|
|
605
|
-
return typeof window !== 'undefined' && 'AudioContext' in window;
|
|
606
|
-
case 'gamepad':
|
|
607
|
-
return typeof navigator !== 'undefined' && 'getGamepads' in navigator;
|
|
608
|
-
case 'webBluetooth':
|
|
609
|
-
return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
|
|
610
|
-
case 'webUsb':
|
|
611
|
-
return typeof navigator !== 'undefined' && 'usb' in navigator;
|
|
612
|
-
case 'webNfc':
|
|
613
|
-
return typeof window !== 'undefined' && 'NDEFReader' in window;
|
|
614
|
-
case 'paymentRequest':
|
|
615
|
-
return typeof window !== 'undefined' && 'PaymentRequest' in window;
|
|
616
|
-
case 'credentialManagement':
|
|
617
|
-
return typeof navigator !== 'undefined' && 'credentials' in navigator;
|
|
618
|
-
default:
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
getAllStatuses() {
|
|
623
|
-
const secureContext = this.isSecureContext();
|
|
624
|
-
return this.getCapabilities().map((capability) => ({
|
|
625
|
-
id: capability.id,
|
|
626
|
-
label: capability.label,
|
|
627
|
-
supported: this.isSupported(capability.id),
|
|
628
|
-
secureContext,
|
|
629
|
-
requiresSecureContext: capability.requiresSecureContext,
|
|
630
|
-
}));
|
|
631
|
-
}
|
|
632
|
-
async getPermissionState(permission) {
|
|
633
|
-
if (typeof navigator === 'undefined' || !('permissions' in navigator)) {
|
|
634
|
-
return 'unknown';
|
|
644
|
+
}
|
|
645
|
+
async writeText(text) {
|
|
646
|
+
this.ensureSupported();
|
|
647
|
+
try {
|
|
648
|
+
await navigator.clipboard.writeText(text);
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
this.logError('Error writing to clipboard:', error);
|
|
652
|
+
throw error;
|
|
635
653
|
}
|
|
654
|
+
}
|
|
655
|
+
async readText() {
|
|
656
|
+
this.ensureSupported();
|
|
636
657
|
try {
|
|
637
|
-
|
|
638
|
-
return status.state;
|
|
658
|
+
return await navigator.clipboard.readText();
|
|
639
659
|
}
|
|
640
|
-
catch {
|
|
641
|
-
|
|
660
|
+
catch (error) {
|
|
661
|
+
this.logError('Error reading from clipboard:', error);
|
|
662
|
+
throw error;
|
|
642
663
|
}
|
|
643
664
|
}
|
|
644
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type:
|
|
645
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type:
|
|
665
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
666
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService });
|
|
646
667
|
}
|
|
647
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type:
|
|
648
|
-
type: Injectable
|
|
649
|
-
args: [{ providedIn: 'root' }]
|
|
668
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ClipboardService, decorators: [{
|
|
669
|
+
type: Injectable
|
|
650
670
|
}] });
|
|
651
671
|
|
|
652
672
|
class BatteryService extends BrowserApiBaseService {
|
|
@@ -654,12 +674,8 @@ class BatteryService extends BrowserApiBaseService {
|
|
|
654
674
|
getApiName() {
|
|
655
675
|
return 'battery';
|
|
656
676
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const nav = navigator;
|
|
660
|
-
if (!('getBattery' in nav)) {
|
|
661
|
-
throw new Error('Battery Status API not supported in this browser');
|
|
662
|
-
}
|
|
677
|
+
getCapabilityId() {
|
|
678
|
+
return 'battery';
|
|
663
679
|
}
|
|
664
680
|
async initialize() {
|
|
665
681
|
this.ensureSupported();
|
|
@@ -739,11 +755,8 @@ class WebShareService extends BrowserApiBaseService {
|
|
|
739
755
|
getApiName() {
|
|
740
756
|
return 'web-share';
|
|
741
757
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (!('share' in navigator)) {
|
|
745
|
-
throw new Error('Web Share API not supported in this browser');
|
|
746
|
-
}
|
|
758
|
+
getCapabilityId() {
|
|
759
|
+
return 'webShare';
|
|
747
760
|
}
|
|
748
761
|
async share(data) {
|
|
749
762
|
this.ensureSupported();
|
|
@@ -998,11 +1011,8 @@ class WebStorageService extends BrowserApiBaseService {
|
|
|
998
1011
|
getApiName() {
|
|
999
1012
|
return 'storage';
|
|
1000
1013
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (typeof Storage === 'undefined') {
|
|
1004
|
-
throw new Error('Storage API not supported in this browser');
|
|
1005
|
-
}
|
|
1014
|
+
getCapabilityId() {
|
|
1015
|
+
return 'webStorage';
|
|
1006
1016
|
}
|
|
1007
1017
|
/** Returns true if either local or session storage is usable. */
|
|
1008
1018
|
isSupported() {
|
|
@@ -1122,7 +1132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
1122
1132
|
}], ctorParameters: () => [] });
|
|
1123
1133
|
|
|
1124
1134
|
const DEFAULT_MAX_RECONNECT_DELAY = 30_000;
|
|
1125
|
-
const DEFAULT_REQUEST_TIMEOUT = 30_000;
|
|
1135
|
+
const DEFAULT_REQUEST_TIMEOUT$1 = 30_000;
|
|
1126
1136
|
/**
|
|
1127
1137
|
* Stateful WebSocket client wrapping a single connection. One instance per logical
|
|
1128
1138
|
* connection (do NOT share between `connect()` calls).
|
|
@@ -1194,7 +1204,7 @@ class WebSocketClient {
|
|
|
1194
1204
|
*/
|
|
1195
1205
|
request(type, data, opts) {
|
|
1196
1206
|
const id = this.generateId();
|
|
1197
|
-
const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
1207
|
+
const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT$1;
|
|
1198
1208
|
return new Promise((resolve, reject) => {
|
|
1199
1209
|
const timer = setTimeout(() => {
|
|
1200
1210
|
this.pendingRequests.delete(id);
|
|
@@ -1406,11 +1416,8 @@ class WebSocketService extends BrowserApiBaseService {
|
|
|
1406
1416
|
getApiName() {
|
|
1407
1417
|
return 'websocket';
|
|
1408
1418
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
if (typeof WebSocket === 'undefined') {
|
|
1412
|
-
throw new Error('WebSocket API not supported in this browser');
|
|
1413
|
-
}
|
|
1419
|
+
getCapabilityId() {
|
|
1420
|
+
return 'webSocket';
|
|
1414
1421
|
}
|
|
1415
1422
|
/**
|
|
1416
1423
|
* Create a new WebSocket client. The client owns one connection and is the recommended
|
|
@@ -1568,162 +1575,259 @@ function toObservableLike(client) {
|
|
|
1568
1575
|
});
|
|
1569
1576
|
}
|
|
1570
1577
|
|
|
1578
|
+
const DEFAULT_REQUEST_TIMEOUT = 30_000;
|
|
1579
|
+
/**
|
|
1580
|
+
* Service for creating and managing Web Workers with first-class support for
|
|
1581
|
+
* request/response over `postMessage` (id correlation, timeout, transferables).
|
|
1582
|
+
*
|
|
1583
|
+
* Status is exposed both as a `Signal<WorkerStatus>` (preferred via
|
|
1584
|
+
* {@link getStatusSignal}) and as an `Observable<WorkerStatus>` for backward
|
|
1585
|
+
* compatibility ({@link createWorker}, {@link getStatus}).
|
|
1586
|
+
*
|
|
1587
|
+
* The service registers a single `DestroyRef.onDestroy` in its constructor that
|
|
1588
|
+
* terminates every registered worker; per-worker handlers do not register their
|
|
1589
|
+
* own cleanup callbacks (avoids accumulating callbacks per `setupWorker` call,
|
|
1590
|
+
* a previous leak source).
|
|
1591
|
+
*/
|
|
1571
1592
|
class WebWorkerService extends BrowserApiBaseService {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
workerMessages = new Map();
|
|
1575
|
-
currentWorkerStatuses = new Map();
|
|
1593
|
+
workerLogger = inject(BROWSER_API_LOGGER);
|
|
1594
|
+
entries = new Map();
|
|
1576
1595
|
_cleanup = this.destroyRef.onDestroy(() => this.terminateAllWorkers());
|
|
1577
1596
|
getApiName() {
|
|
1578
1597
|
return 'webworker';
|
|
1579
1598
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
if (typeof Worker === 'undefined') {
|
|
1583
|
-
throw new Error('Web Workers not supported in this browser');
|
|
1584
|
-
}
|
|
1599
|
+
getCapabilityId() {
|
|
1600
|
+
return 'webWorker';
|
|
1585
1601
|
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Create a worker. Idempotent: calling twice with the same name returns the
|
|
1604
|
+
* existing entry without recreating the worker.
|
|
1605
|
+
*
|
|
1606
|
+
* Returns an `Observable<WorkerStatus>` for backward compatibility. Prefer
|
|
1607
|
+
* {@link createWorkerSignal} for new code.
|
|
1608
|
+
*/
|
|
1586
1609
|
createWorker(name, scriptUrl) {
|
|
1610
|
+
const status = this.createWorkerSignal(name, scriptUrl);
|
|
1611
|
+
return toObservable(status);
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Create a worker (signal-first). Returns the status signal; status is also
|
|
1615
|
+
* accessible later via {@link getStatusSignal}.
|
|
1616
|
+
*/
|
|
1617
|
+
createWorkerSignal(name, scriptUrl) {
|
|
1587
1618
|
this.ensureSupported();
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
this.logError(`Failed to create worker ${name}:`, error);
|
|
1610
|
-
const status = {
|
|
1611
|
-
initialized: false,
|
|
1612
|
-
running: false,
|
|
1613
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1614
|
-
messageCount: 0,
|
|
1615
|
-
};
|
|
1616
|
-
this.currentWorkerStatuses.set(name, status);
|
|
1617
|
-
this.updateWorkerStatus(name, status);
|
|
1618
|
-
observer.next(status);
|
|
1619
|
-
}
|
|
1620
|
-
return () => {
|
|
1621
|
-
// No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
|
|
1622
|
-
};
|
|
1623
|
-
});
|
|
1619
|
+
const existing = this.entries.get(name);
|
|
1620
|
+
if (existing && existing.worker)
|
|
1621
|
+
return existing.status.asReadonly();
|
|
1622
|
+
const entry = this.ensureEntry(name);
|
|
1623
|
+
try {
|
|
1624
|
+
const worker = new Worker(scriptUrl);
|
|
1625
|
+
entry.worker = worker;
|
|
1626
|
+
this.attachHandlers(name, entry);
|
|
1627
|
+
entry.status.set({ initialized: true, running: true, messageCount: 0 });
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1631
|
+
entry.status.set({
|
|
1632
|
+
initialized: false,
|
|
1633
|
+
running: false,
|
|
1634
|
+
error: message,
|
|
1635
|
+
messageCount: 0,
|
|
1636
|
+
});
|
|
1637
|
+
this.workerLogger.error(`[webworker] Failed to create worker "${name}"`, error);
|
|
1638
|
+
}
|
|
1639
|
+
return entry.status.asReadonly();
|
|
1624
1640
|
}
|
|
1625
1641
|
terminateWorker(name) {
|
|
1626
|
-
const
|
|
1627
|
-
if (
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1642
|
+
const entry = this.entries.get(name);
|
|
1643
|
+
if (!entry)
|
|
1644
|
+
return;
|
|
1645
|
+
this.rejectPending(entry, new Error(`Worker "${name}" terminated`));
|
|
1646
|
+
if (entry.worker)
|
|
1647
|
+
entry.worker.terminate();
|
|
1648
|
+
entry.messages$.complete();
|
|
1649
|
+
this.entries.delete(name);
|
|
1634
1650
|
}
|
|
1635
1651
|
terminateAllWorkers() {
|
|
1636
|
-
this.
|
|
1652
|
+
for (const name of [...this.entries.keys()]) {
|
|
1637
1653
|
this.terminateWorker(name);
|
|
1638
|
-
}
|
|
1654
|
+
}
|
|
1639
1655
|
}
|
|
1656
|
+
/** Send a fire-and-forget message. Use {@link request} when you need a reply. */
|
|
1640
1657
|
postMessage(workerName, task) {
|
|
1641
|
-
const
|
|
1642
|
-
if (!worker) {
|
|
1643
|
-
this.
|
|
1658
|
+
const entry = this.entries.get(workerName);
|
|
1659
|
+
if (!entry || !entry.worker) {
|
|
1660
|
+
this.workerLogger.error(`[webworker] postMessage: worker "${workerName}" not found`);
|
|
1644
1661
|
return;
|
|
1645
1662
|
}
|
|
1663
|
+
const message = {
|
|
1664
|
+
id: task.id ?? this.generateId(),
|
|
1665
|
+
type: task.type,
|
|
1666
|
+
data: task.data,
|
|
1667
|
+
timestamp: Date.now(),
|
|
1668
|
+
};
|
|
1646
1669
|
try {
|
|
1647
|
-
const message = { ...task, timestamp: Date.now() };
|
|
1648
1670
|
if (task.transferable) {
|
|
1649
|
-
worker.postMessage(message, task.transferable);
|
|
1671
|
+
entry.worker.postMessage(message, task.transferable);
|
|
1650
1672
|
}
|
|
1651
1673
|
else {
|
|
1652
|
-
worker.postMessage(message);
|
|
1653
|
-
}
|
|
1654
|
-
const currentStatus = this.currentWorkerStatuses.get(workerName);
|
|
1655
|
-
if (currentStatus) {
|
|
1656
|
-
currentStatus.messageCount++;
|
|
1657
|
-
this.updateWorkerStatus(workerName, currentStatus);
|
|
1674
|
+
entry.worker.postMessage(message);
|
|
1658
1675
|
}
|
|
1676
|
+
this.bumpMessageCount(entry);
|
|
1659
1677
|
}
|
|
1660
1678
|
catch (error) {
|
|
1661
|
-
this.
|
|
1679
|
+
this.workerLogger.error(`[webworker] postMessage failed for "${workerName}"`, error);
|
|
1662
1680
|
}
|
|
1663
1681
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1682
|
+
/**
|
|
1683
|
+
* Send a message and await a correlated response. The worker MUST send back a
|
|
1684
|
+
* message containing `correlationId` matching the request id.
|
|
1685
|
+
*
|
|
1686
|
+
* ```ts
|
|
1687
|
+
* const result = await ws.request<{ ok: boolean }>('worker', 'compute', { n: 1 });
|
|
1688
|
+
* ```
|
|
1689
|
+
*/
|
|
1690
|
+
request(workerName, type, data, opts) {
|
|
1691
|
+
const entry = this.entries.get(workerName);
|
|
1692
|
+
if (!entry || !entry.worker) {
|
|
1693
|
+
return Promise.reject(new Error(`Worker "${workerName}" not found`));
|
|
1667
1694
|
}
|
|
1668
|
-
|
|
1695
|
+
const id = this.generateId();
|
|
1696
|
+
const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
1697
|
+
return new Promise((resolve, reject) => {
|
|
1698
|
+
const timer = setTimeout(() => {
|
|
1699
|
+
entry.pending.delete(id);
|
|
1700
|
+
reject(new Error(`WebWorker "${workerName}" request timeout after ${timeoutMs}ms`));
|
|
1701
|
+
}, timeoutMs);
|
|
1702
|
+
entry.pending.set(id, {
|
|
1703
|
+
resolve: resolve,
|
|
1704
|
+
reject,
|
|
1705
|
+
timer,
|
|
1706
|
+
});
|
|
1707
|
+
const message = {
|
|
1708
|
+
id,
|
|
1709
|
+
type,
|
|
1710
|
+
data,
|
|
1711
|
+
timestamp: Date.now(),
|
|
1712
|
+
correlationId: id,
|
|
1713
|
+
};
|
|
1714
|
+
try {
|
|
1715
|
+
if (opts?.transferable) {
|
|
1716
|
+
entry.worker.postMessage(message, opts.transferable);
|
|
1717
|
+
}
|
|
1718
|
+
else {
|
|
1719
|
+
entry.worker.postMessage(message);
|
|
1720
|
+
}
|
|
1721
|
+
this.bumpMessageCount(entry);
|
|
1722
|
+
}
|
|
1723
|
+
catch (error) {
|
|
1724
|
+
clearTimeout(timer);
|
|
1725
|
+
entry.pending.delete(id);
|
|
1726
|
+
reject(error instanceof Error ? error : new Error('postMessage failed'));
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
getMessages(workerName) {
|
|
1731
|
+
const entry = this.ensureEntry(workerName);
|
|
1732
|
+
return entry.messages$.asObservable();
|
|
1733
|
+
}
|
|
1734
|
+
getMessagesByType(workerName, type) {
|
|
1735
|
+
return this.getMessages(workerName).pipe(filter((m) => m.type === type));
|
|
1669
1736
|
}
|
|
1737
|
+
/** @deprecated Use {@link getStatusSignal}. Kept as Observable for backward compat. */
|
|
1670
1738
|
getStatus(workerName) {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
return this.
|
|
1739
|
+
return toObservable(this.getStatusSignal(workerName));
|
|
1740
|
+
}
|
|
1741
|
+
getStatusSignal(workerName) {
|
|
1742
|
+
return this.ensureEntry(workerName).status.asReadonly();
|
|
1675
1743
|
}
|
|
1676
1744
|
getCurrentStatus(workerName) {
|
|
1677
|
-
return this.
|
|
1745
|
+
return this.entries.get(workerName)?.status();
|
|
1678
1746
|
}
|
|
1679
1747
|
getAllStatuses() {
|
|
1680
|
-
|
|
1748
|
+
const result = new Map();
|
|
1749
|
+
for (const [name, entry] of this.entries)
|
|
1750
|
+
result.set(name, entry.status());
|
|
1751
|
+
return result;
|
|
1681
1752
|
}
|
|
1682
1753
|
isWorkerRunning(workerName) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1754
|
+
return this.entries.get(workerName)?.status().running ?? false;
|
|
1755
|
+
}
|
|
1756
|
+
getNativeWorker(name) {
|
|
1757
|
+
return this.entries.get(name)?.worker ?? undefined;
|
|
1758
|
+
}
|
|
1759
|
+
getAllWorkers() {
|
|
1760
|
+
const result = new Map();
|
|
1761
|
+
for (const [name, entry] of this.entries) {
|
|
1762
|
+
if (entry.worker)
|
|
1763
|
+
result.set(name, entry.worker);
|
|
1764
|
+
}
|
|
1765
|
+
return result;
|
|
1685
1766
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
1767
|
+
// ---------- internals ----------
|
|
1768
|
+
attachHandlers(name, entry) {
|
|
1769
|
+
if (!entry.worker)
|
|
1770
|
+
return;
|
|
1771
|
+
entry.worker.onmessage = (event) => {
|
|
1772
|
+
const data = event.data ?? {};
|
|
1773
|
+
const correlationId = data.correlationId ?? data.id;
|
|
1774
|
+
if (correlationId && entry.pending.has(correlationId)) {
|
|
1775
|
+
const p = entry.pending.get(correlationId);
|
|
1776
|
+
clearTimeout(p.timer);
|
|
1777
|
+
entry.pending.delete(correlationId);
|
|
1778
|
+
p.resolve(data.data);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1688
1781
|
const message = {
|
|
1689
|
-
id:
|
|
1690
|
-
type:
|
|
1691
|
-
data:
|
|
1692
|
-
timestamp:
|
|
1782
|
+
id: data.id ?? this.generateId(),
|
|
1783
|
+
type: data.type ?? 'message',
|
|
1784
|
+
data: data.data,
|
|
1785
|
+
timestamp: data.timestamp ?? Date.now(),
|
|
1786
|
+
correlationId: data.correlationId,
|
|
1693
1787
|
};
|
|
1694
|
-
|
|
1695
|
-
this.workerMessages.set(name, new Subject());
|
|
1696
|
-
}
|
|
1697
|
-
this.workerMessages.get(name).next(message);
|
|
1788
|
+
entry.messages$.next(message);
|
|
1698
1789
|
};
|
|
1699
|
-
worker.onerror = (error) => {
|
|
1700
|
-
this.
|
|
1701
|
-
|
|
1702
|
-
|
|
1790
|
+
entry.worker.onerror = (error) => {
|
|
1791
|
+
this.workerLogger.error(`[webworker] Worker "${name}" error`, error);
|
|
1792
|
+
entry.status.update((s) => ({
|
|
1793
|
+
...s,
|
|
1703
1794
|
running: false,
|
|
1704
1795
|
error: error instanceof Error ? error.message : 'Worker error',
|
|
1705
|
-
|
|
1706
|
-
};
|
|
1707
|
-
this.currentWorkerStatuses.set(name, status);
|
|
1708
|
-
this.updateWorkerStatus(name, status);
|
|
1796
|
+
}));
|
|
1709
1797
|
};
|
|
1710
|
-
// Auto-cleanup when service is destroyed
|
|
1711
|
-
this.destroyRef.onDestroy(() => {
|
|
1712
|
-
this.terminateWorker(name);
|
|
1713
|
-
});
|
|
1714
1798
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1799
|
+
rejectPending(entry, reason) {
|
|
1800
|
+
for (const p of entry.pending.values()) {
|
|
1801
|
+
clearTimeout(p.timer);
|
|
1802
|
+
p.reject(reason);
|
|
1718
1803
|
}
|
|
1719
|
-
|
|
1804
|
+
entry.pending.clear();
|
|
1720
1805
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
return this.workers.get(name);
|
|
1806
|
+
bumpMessageCount(entry) {
|
|
1807
|
+
entry.status.update((s) => ({ ...s, messageCount: s.messageCount + 1 }));
|
|
1724
1808
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1809
|
+
ensureEntry(workerName) {
|
|
1810
|
+
let entry = this.entries.get(workerName);
|
|
1811
|
+
if (!entry) {
|
|
1812
|
+
entry = {
|
|
1813
|
+
worker: null,
|
|
1814
|
+
status: signal({
|
|
1815
|
+
initialized: false,
|
|
1816
|
+
running: false,
|
|
1817
|
+
messageCount: 0,
|
|
1818
|
+
}),
|
|
1819
|
+
messages$: new Subject(),
|
|
1820
|
+
pending: new Map(),
|
|
1821
|
+
};
|
|
1822
|
+
this.entries.set(workerName, entry);
|
|
1823
|
+
}
|
|
1824
|
+
return entry;
|
|
1825
|
+
}
|
|
1826
|
+
generateId() {
|
|
1827
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
1828
|
+
return globalThis.crypto.randomUUID();
|
|
1829
|
+
}
|
|
1830
|
+
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1727
1831
|
}
|
|
1728
1832
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1729
1833
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService });
|
|
@@ -1763,8 +1867,8 @@ class IntersectionObserverService extends BrowserApiBaseService {
|
|
|
1763
1867
|
getApiName() {
|
|
1764
1868
|
return 'intersection-observer';
|
|
1765
1869
|
}
|
|
1766
|
-
|
|
1767
|
-
return
|
|
1870
|
+
getCapabilityId() {
|
|
1871
|
+
return 'intersectionObserver';
|
|
1768
1872
|
}
|
|
1769
1873
|
observe(element, options = {}) {
|
|
1770
1874
|
if (!this.isSupported()) {
|
|
@@ -1823,8 +1927,8 @@ class ResizeObserverService extends BrowserApiBaseService {
|
|
|
1823
1927
|
getApiName() {
|
|
1824
1928
|
return 'resize-observer';
|
|
1825
1929
|
}
|
|
1826
|
-
|
|
1827
|
-
return
|
|
1930
|
+
getCapabilityId() {
|
|
1931
|
+
return 'resizeObserver';
|
|
1828
1932
|
}
|
|
1829
1933
|
observe(element, options = {}) {
|
|
1830
1934
|
if (!this.isSupported()) {
|
|
@@ -1864,8 +1968,8 @@ class PageVisibilityService extends BrowserApiBaseService {
|
|
|
1864
1968
|
getApiName() {
|
|
1865
1969
|
return 'page-visibility';
|
|
1866
1970
|
}
|
|
1867
|
-
|
|
1868
|
-
return
|
|
1971
|
+
getCapabilityId() {
|
|
1972
|
+
return 'pageVisibility';
|
|
1869
1973
|
}
|
|
1870
1974
|
get isHidden() {
|
|
1871
1975
|
if (!this.isSupported())
|
|
@@ -1941,13 +2045,11 @@ class BroadcastChannelService extends ConnectionRegistryBaseService {
|
|
|
1941
2045
|
closeNativeConnection(channel) {
|
|
1942
2046
|
channel.close();
|
|
1943
2047
|
}
|
|
1944
|
-
|
|
1945
|
-
return
|
|
2048
|
+
getCapabilityId() {
|
|
2049
|
+
return 'broadcastChannel';
|
|
1946
2050
|
}
|
|
1947
2051
|
ensureBroadcastChannelSupported() {
|
|
1948
|
-
|
|
1949
|
-
throw new Error('BroadcastChannel API not supported in this environment');
|
|
1950
|
-
}
|
|
2052
|
+
this.ensureSupported();
|
|
1951
2053
|
}
|
|
1952
2054
|
open(name) {
|
|
1953
2055
|
this.ensureBroadcastChannelSupported();
|
|
@@ -2044,8 +2146,8 @@ class NetworkInformationService extends BrowserApiBaseService {
|
|
|
2044
2146
|
getApiName() {
|
|
2045
2147
|
return 'network-information';
|
|
2046
2148
|
}
|
|
2047
|
-
|
|
2048
|
-
return
|
|
2149
|
+
getCapabilityId() {
|
|
2150
|
+
return 'networkInformation';
|
|
2049
2151
|
}
|
|
2050
2152
|
getSnapshot() {
|
|
2051
2153
|
return this.isBrowserEnvironment() ? getNetworkSnapshot() : { online: true };
|
|
@@ -2068,8 +2170,8 @@ class ScreenWakeLockService extends BrowserApiBaseService {
|
|
|
2068
2170
|
getApiName() {
|
|
2069
2171
|
return 'screen-wake-lock';
|
|
2070
2172
|
}
|
|
2071
|
-
|
|
2072
|
-
return
|
|
2173
|
+
getCapabilityId() {
|
|
2174
|
+
return 'screenWakeLock';
|
|
2073
2175
|
}
|
|
2074
2176
|
get isActive() {
|
|
2075
2177
|
return this.sentinel !== null && !this.sentinel.released;
|
|
@@ -2163,8 +2265,8 @@ class ScreenOrientationService extends BrowserApiBaseService {
|
|
|
2163
2265
|
getApiName() {
|
|
2164
2266
|
return 'screen-orientation';
|
|
2165
2267
|
}
|
|
2166
|
-
|
|
2167
|
-
return
|
|
2268
|
+
getCapabilityId() {
|
|
2269
|
+
return 'screenOrientation';
|
|
2168
2270
|
}
|
|
2169
2271
|
getSnapshot() {
|
|
2170
2272
|
return this.isBrowserEnvironment()
|
|
@@ -2208,8 +2310,12 @@ class FullscreenService extends BrowserApiBaseService {
|
|
|
2208
2310
|
getApiName() {
|
|
2209
2311
|
return 'fullscreen';
|
|
2210
2312
|
}
|
|
2313
|
+
getCapabilityId() {
|
|
2314
|
+
return 'fullscreen';
|
|
2315
|
+
}
|
|
2316
|
+
/** Override to also check the *enabled* flag (browser may have disabled fullscreen). */
|
|
2211
2317
|
isSupported() {
|
|
2212
|
-
if (!
|
|
2318
|
+
if (!super.isSupported())
|
|
2213
2319
|
return false;
|
|
2214
2320
|
return !!(document.fullscreenEnabled ??
|
|
2215
2321
|
document.webkitFullscreenEnabled);
|
|
@@ -2300,17 +2406,18 @@ class FileSystemAccessService extends BrowserApiBaseService {
|
|
|
2300
2406
|
getApiName() {
|
|
2301
2407
|
return 'file-system-access';
|
|
2302
2408
|
}
|
|
2409
|
+
getCapabilityId() {
|
|
2410
|
+
return 'fileSystemAccess';
|
|
2411
|
+
}
|
|
2412
|
+
/** Override to also assert secure context (required by the spec). */
|
|
2303
2413
|
isSupported() {
|
|
2304
|
-
return
|
|
2414
|
+
return super.isSupported() && typeof window !== 'undefined' && window.isSecureContext;
|
|
2305
2415
|
}
|
|
2306
2416
|
get win() {
|
|
2307
2417
|
return window;
|
|
2308
2418
|
}
|
|
2309
2419
|
ensureSupported() {
|
|
2310
2420
|
super.ensureSupported();
|
|
2311
|
-
if (!('showOpenFilePicker' in window)) {
|
|
2312
|
-
throw new Error('File System Access API not supported in this browser');
|
|
2313
|
-
}
|
|
2314
2421
|
if (!window.isSecureContext) {
|
|
2315
2422
|
throw new Error('File System Access API requires a secure context (HTTPS)');
|
|
2316
2423
|
}
|
|
@@ -2386,8 +2493,8 @@ class MediaRecorderService extends BrowserApiBaseService {
|
|
|
2386
2493
|
getApiName() {
|
|
2387
2494
|
return 'media-recorder';
|
|
2388
2495
|
}
|
|
2389
|
-
|
|
2390
|
-
return
|
|
2496
|
+
getCapabilityId() {
|
|
2497
|
+
return 'mediaRecorder';
|
|
2391
2498
|
}
|
|
2392
2499
|
get state() {
|
|
2393
2500
|
return this.recorder?.state ?? 'inactive';
|
|
@@ -2477,13 +2584,11 @@ class ServerSentEventsService extends ConnectionRegistryBaseService {
|
|
|
2477
2584
|
closeNativeConnection(source) {
|
|
2478
2585
|
source.close();
|
|
2479
2586
|
}
|
|
2480
|
-
|
|
2481
|
-
return
|
|
2587
|
+
getCapabilityId() {
|
|
2588
|
+
return 'serverSentEvents';
|
|
2482
2589
|
}
|
|
2483
2590
|
ensureSSESupported() {
|
|
2484
|
-
|
|
2485
|
-
throw new Error('Server-Sent Events (EventSource) not supported in this environment');
|
|
2486
|
-
}
|
|
2591
|
+
this.ensureSupported();
|
|
2487
2592
|
}
|
|
2488
2593
|
connect(url, config = {}) {
|
|
2489
2594
|
this.ensureSSESupported();
|
|
@@ -2563,8 +2668,8 @@ class VibrationService extends BrowserApiBaseService {
|
|
|
2563
2668
|
notification: [200],
|
|
2564
2669
|
doubleTap: [50, 100, 50],
|
|
2565
2670
|
};
|
|
2566
|
-
|
|
2567
|
-
return
|
|
2671
|
+
getCapabilityId() {
|
|
2672
|
+
return 'vibration';
|
|
2568
2673
|
}
|
|
2569
2674
|
vibrate(pattern = 200) {
|
|
2570
2675
|
if (!this.isSupported())
|
|
@@ -2597,13 +2702,11 @@ class SpeechSynthesisService extends BrowserApiBaseService {
|
|
|
2597
2702
|
getApiName() {
|
|
2598
2703
|
return 'speech-synthesis';
|
|
2599
2704
|
}
|
|
2600
|
-
|
|
2601
|
-
return
|
|
2705
|
+
getCapabilityId() {
|
|
2706
|
+
return 'speechSynthesis';
|
|
2602
2707
|
}
|
|
2603
2708
|
ensureSpeechSynthesisSupported() {
|
|
2604
|
-
|
|
2605
|
-
throw new Error('Speech Synthesis API not supported in this browser');
|
|
2606
|
-
}
|
|
2709
|
+
this.ensureSupported();
|
|
2607
2710
|
}
|
|
2608
2711
|
get state() {
|
|
2609
2712
|
if (!this.isSupported())
|
|
@@ -2713,8 +2816,8 @@ class MutationObserverService extends BrowserApiBaseService {
|
|
|
2713
2816
|
getApiName() {
|
|
2714
2817
|
return 'mutation-observer';
|
|
2715
2818
|
}
|
|
2716
|
-
|
|
2717
|
-
return
|
|
2819
|
+
getCapabilityId() {
|
|
2820
|
+
return 'mutationObserver';
|
|
2718
2821
|
}
|
|
2719
2822
|
observe(target, options) {
|
|
2720
2823
|
if (!this.isSupported()) {
|
|
@@ -2751,8 +2854,8 @@ class PerformanceObserverService extends BrowserApiBaseService {
|
|
|
2751
2854
|
getApiName() {
|
|
2752
2855
|
return 'performance-observer';
|
|
2753
2856
|
}
|
|
2754
|
-
|
|
2755
|
-
return
|
|
2857
|
+
getCapabilityId() {
|
|
2858
|
+
return 'performanceObserver';
|
|
2756
2859
|
}
|
|
2757
2860
|
observe(config) {
|
|
2758
2861
|
if (!this.isSupported()) {
|
|
@@ -2780,8 +2883,8 @@ class WebAudioService extends BrowserApiBaseService {
|
|
|
2780
2883
|
return 'web-audio';
|
|
2781
2884
|
}
|
|
2782
2885
|
context = null;
|
|
2783
|
-
|
|
2784
|
-
return
|
|
2886
|
+
getCapabilityId() {
|
|
2887
|
+
return 'webAudio';
|
|
2785
2888
|
}
|
|
2786
2889
|
getContext() {
|
|
2787
2890
|
if (!this.isSupported()) {
|
|
@@ -2942,8 +3045,8 @@ class GamepadService extends BrowserApiBaseService {
|
|
|
2942
3045
|
getApiName() {
|
|
2943
3046
|
return 'gamepad';
|
|
2944
3047
|
}
|
|
2945
|
-
|
|
2946
|
-
return
|
|
3048
|
+
getCapabilityId() {
|
|
3049
|
+
return 'gamepad';
|
|
2947
3050
|
}
|
|
2948
3051
|
getSnapshot(index) {
|
|
2949
3052
|
if (!this.isSupported())
|
|
@@ -3163,6 +3266,257 @@ function injectGamepad(index, intervalMs = 16) {
|
|
|
3163
3266
|
};
|
|
3164
3267
|
}
|
|
3165
3268
|
|
|
3269
|
+
function injectClipboard() {
|
|
3270
|
+
const destroyRef = inject(DestroyRef);
|
|
3271
|
+
const platformId = inject(PLATFORM_ID);
|
|
3272
|
+
const isBrowser = isPlatformBrowser(platformId);
|
|
3273
|
+
const supported = signal(isBrowser && typeof navigator !== 'undefined' && !!navigator.clipboard, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
|
|
3274
|
+
const text = signal(null, ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
|
|
3275
|
+
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
3276
|
+
const busy = signal(false, ...(ngDevMode ? [{ debugName: "busy" }] : /* istanbul ignore next */ []));
|
|
3277
|
+
let disposed = false;
|
|
3278
|
+
destroyRef.onDestroy(() => {
|
|
3279
|
+
disposed = true;
|
|
3280
|
+
});
|
|
3281
|
+
const writeText = async (value) => {
|
|
3282
|
+
if (!supported() || disposed) {
|
|
3283
|
+
error.set('Clipboard API not supported');
|
|
3284
|
+
return false;
|
|
3285
|
+
}
|
|
3286
|
+
busy.set(true);
|
|
3287
|
+
try {
|
|
3288
|
+
await navigator.clipboard.writeText(value);
|
|
3289
|
+
if (!disposed) {
|
|
3290
|
+
text.set(value);
|
|
3291
|
+
error.set(null);
|
|
3292
|
+
}
|
|
3293
|
+
return true;
|
|
3294
|
+
}
|
|
3295
|
+
catch (err) {
|
|
3296
|
+
if (!disposed)
|
|
3297
|
+
error.set(err instanceof Error ? err.message : 'writeText failed');
|
|
3298
|
+
return false;
|
|
3299
|
+
}
|
|
3300
|
+
finally {
|
|
3301
|
+
if (!disposed)
|
|
3302
|
+
busy.set(false);
|
|
3303
|
+
}
|
|
3304
|
+
};
|
|
3305
|
+
const readText = async () => {
|
|
3306
|
+
if (!supported() || disposed) {
|
|
3307
|
+
error.set('Clipboard API not supported');
|
|
3308
|
+
return null;
|
|
3309
|
+
}
|
|
3310
|
+
busy.set(true);
|
|
3311
|
+
try {
|
|
3312
|
+
const value = await navigator.clipboard.readText();
|
|
3313
|
+
if (!disposed) {
|
|
3314
|
+
text.set(value);
|
|
3315
|
+
error.set(null);
|
|
3316
|
+
}
|
|
3317
|
+
return value;
|
|
3318
|
+
}
|
|
3319
|
+
catch (err) {
|
|
3320
|
+
if (!disposed)
|
|
3321
|
+
error.set(err instanceof Error ? err.message : 'readText failed');
|
|
3322
|
+
return null;
|
|
3323
|
+
}
|
|
3324
|
+
finally {
|
|
3325
|
+
if (!disposed)
|
|
3326
|
+
busy.set(false);
|
|
3327
|
+
}
|
|
3328
|
+
};
|
|
3329
|
+
return {
|
|
3330
|
+
text: text.asReadonly(),
|
|
3331
|
+
error: error.asReadonly(),
|
|
3332
|
+
busy: busy.asReadonly(),
|
|
3333
|
+
isSupported: supported.asReadonly(),
|
|
3334
|
+
writeText,
|
|
3335
|
+
readText,
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
function injectGeolocation(opts = {}) {
|
|
3340
|
+
const destroyRef = inject(DestroyRef);
|
|
3341
|
+
const platformId = inject(PLATFORM_ID);
|
|
3342
|
+
const isBrowser = isPlatformBrowser(platformId);
|
|
3343
|
+
const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'geolocation' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
|
|
3344
|
+
const position = signal(null, ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
3345
|
+
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
3346
|
+
const watching = signal(false, ...(ngDevMode ? [{ debugName: "watching" }] : /* istanbul ignore next */ []));
|
|
3347
|
+
let watchId = null;
|
|
3348
|
+
const watch = (positionOpts) => {
|
|
3349
|
+
if (!supported() || watchId !== null)
|
|
3350
|
+
return;
|
|
3351
|
+
watching.set(true);
|
|
3352
|
+
watchId = navigator.geolocation.watchPosition((pos) => {
|
|
3353
|
+
position.set(pos);
|
|
3354
|
+
error.set(null);
|
|
3355
|
+
}, (err) => error.set(err), positionOpts);
|
|
3356
|
+
};
|
|
3357
|
+
const stop = () => {
|
|
3358
|
+
if (watchId !== null) {
|
|
3359
|
+
navigator.geolocation.clearWatch(watchId);
|
|
3360
|
+
watchId = null;
|
|
3361
|
+
}
|
|
3362
|
+
watching.set(false);
|
|
3363
|
+
};
|
|
3364
|
+
const getCurrent = (positionOpts) => {
|
|
3365
|
+
if (!supported()) {
|
|
3366
|
+
return Promise.reject(new Error('Geolocation API not supported'));
|
|
3367
|
+
}
|
|
3368
|
+
return new Promise((resolve, reject) => {
|
|
3369
|
+
navigator.geolocation.getCurrentPosition((pos) => {
|
|
3370
|
+
position.set(pos);
|
|
3371
|
+
error.set(null);
|
|
3372
|
+
resolve(pos);
|
|
3373
|
+
}, (err) => {
|
|
3374
|
+
error.set(err);
|
|
3375
|
+
reject(err);
|
|
3376
|
+
}, positionOpts);
|
|
3377
|
+
});
|
|
3378
|
+
};
|
|
3379
|
+
destroyRef.onDestroy(() => stop());
|
|
3380
|
+
if (opts.watch)
|
|
3381
|
+
watch(opts);
|
|
3382
|
+
return {
|
|
3383
|
+
position: position.asReadonly(),
|
|
3384
|
+
error: error.asReadonly(),
|
|
3385
|
+
watching: watching.asReadonly(),
|
|
3386
|
+
isSupported: supported.asReadonly(),
|
|
3387
|
+
watch,
|
|
3388
|
+
stop,
|
|
3389
|
+
getCurrent,
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
function injectBattery() {
|
|
3394
|
+
const destroyRef = inject(DestroyRef);
|
|
3395
|
+
const platformId = inject(PLATFORM_ID);
|
|
3396
|
+
const isBrowser = isPlatformBrowser(platformId);
|
|
3397
|
+
const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'getBattery' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
|
|
3398
|
+
const info = signal(null, ...(ngDevMode ? [{ debugName: "info" }] : /* istanbul ignore next */ []));
|
|
3399
|
+
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
3400
|
+
let manager = null;
|
|
3401
|
+
let disposed = false;
|
|
3402
|
+
const snapshot = () => manager
|
|
3403
|
+
? {
|
|
3404
|
+
charging: manager.charging,
|
|
3405
|
+
level: manager.level,
|
|
3406
|
+
chargingTime: manager.chargingTime,
|
|
3407
|
+
dischargingTime: manager.dischargingTime,
|
|
3408
|
+
}
|
|
3409
|
+
: null;
|
|
3410
|
+
const update = () => {
|
|
3411
|
+
if (!disposed)
|
|
3412
|
+
info.set(snapshot());
|
|
3413
|
+
};
|
|
3414
|
+
const events = [
|
|
3415
|
+
'chargingchange',
|
|
3416
|
+
'levelchange',
|
|
3417
|
+
'chargingtimechange',
|
|
3418
|
+
'dischargingtimechange',
|
|
3419
|
+
];
|
|
3420
|
+
const refresh = async () => {
|
|
3421
|
+
if (!supported() || disposed)
|
|
3422
|
+
return;
|
|
3423
|
+
try {
|
|
3424
|
+
if (!manager) {
|
|
3425
|
+
const nav = navigator;
|
|
3426
|
+
manager = await nav.getBattery();
|
|
3427
|
+
for (const ev of events) {
|
|
3428
|
+
manager.addEventListener(ev, update);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
update();
|
|
3432
|
+
}
|
|
3433
|
+
catch (err) {
|
|
3434
|
+
if (!disposed)
|
|
3435
|
+
error.set(err instanceof Error ? err.message : 'getBattery failed');
|
|
3436
|
+
}
|
|
3437
|
+
};
|
|
3438
|
+
destroyRef.onDestroy(() => {
|
|
3439
|
+
disposed = true;
|
|
3440
|
+
if (manager) {
|
|
3441
|
+
for (const ev of events) {
|
|
3442
|
+
manager.removeEventListener(ev, update);
|
|
3443
|
+
}
|
|
3444
|
+
manager = null;
|
|
3445
|
+
}
|
|
3446
|
+
});
|
|
3447
|
+
if (supported())
|
|
3448
|
+
void refresh();
|
|
3449
|
+
return {
|
|
3450
|
+
info: info.asReadonly(),
|
|
3451
|
+
error: error.asReadonly(),
|
|
3452
|
+
isSupported: supported.asReadonly(),
|
|
3453
|
+
refresh,
|
|
3454
|
+
};
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
function injectWakeLock() {
|
|
3458
|
+
const destroyRef = inject(DestroyRef);
|
|
3459
|
+
const platformId = inject(PLATFORM_ID);
|
|
3460
|
+
const isBrowser = isPlatformBrowser(platformId);
|
|
3461
|
+
const supported = signal(isBrowser && typeof navigator !== 'undefined' && 'wakeLock' in navigator, ...(ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []));
|
|
3462
|
+
const active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
3463
|
+
const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
3464
|
+
let sentinel = null;
|
|
3465
|
+
let disposed = false;
|
|
3466
|
+
const onRelease = () => {
|
|
3467
|
+
if (!disposed)
|
|
3468
|
+
active.set(false);
|
|
3469
|
+
sentinel = null;
|
|
3470
|
+
};
|
|
3471
|
+
const request = async () => {
|
|
3472
|
+
if (!supported() || disposed) {
|
|
3473
|
+
error.set('Screen Wake Lock API not supported');
|
|
3474
|
+
return false;
|
|
3475
|
+
}
|
|
3476
|
+
if (sentinel && !sentinel.released)
|
|
3477
|
+
return true;
|
|
3478
|
+
try {
|
|
3479
|
+
const nav = navigator;
|
|
3480
|
+
sentinel = await nav.wakeLock.request('screen');
|
|
3481
|
+
sentinel.addEventListener('release', onRelease);
|
|
3482
|
+
if (!disposed) {
|
|
3483
|
+
active.set(true);
|
|
3484
|
+
error.set(null);
|
|
3485
|
+
}
|
|
3486
|
+
return true;
|
|
3487
|
+
}
|
|
3488
|
+
catch (err) {
|
|
3489
|
+
if (!disposed)
|
|
3490
|
+
error.set(err instanceof Error ? err.message : 'wakeLock.request failed');
|
|
3491
|
+
return false;
|
|
3492
|
+
}
|
|
3493
|
+
};
|
|
3494
|
+
const release = async () => {
|
|
3495
|
+
if (!sentinel || sentinel.released)
|
|
3496
|
+
return;
|
|
3497
|
+
try {
|
|
3498
|
+
await sentinel.release();
|
|
3499
|
+
}
|
|
3500
|
+
finally {
|
|
3501
|
+
onRelease();
|
|
3502
|
+
}
|
|
3503
|
+
};
|
|
3504
|
+
destroyRef.onDestroy(() => {
|
|
3505
|
+
disposed = true;
|
|
3506
|
+
if (sentinel && !sentinel.released) {
|
|
3507
|
+
void sentinel.release();
|
|
3508
|
+
}
|
|
3509
|
+
sentinel = null;
|
|
3510
|
+
});
|
|
3511
|
+
return {
|
|
3512
|
+
active: active.asReadonly(),
|
|
3513
|
+
error: error.asReadonly(),
|
|
3514
|
+
isSupported: supported.asReadonly(),
|
|
3515
|
+
request,
|
|
3516
|
+
release,
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3166
3520
|
class BrowserSupportUtil {
|
|
3167
3521
|
static isSupported(feature) {
|
|
3168
3522
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
@@ -3484,4 +3838,4 @@ const version = '0.1.0';
|
|
|
3484
3838
|
* Generated bundle index. Do not edit.
|
|
3485
3839
|
*/
|
|
3486
3840
|
|
|
3487
|
-
export { BROWSER_API_LOGGER, BatteryService, BroadcastChannelService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, ConnectionRegistryBaseService, FileSystemAccessService, FullscreenService, GamepadService, GeolocationService, IntersectionObserverService, MediaDevicesService, MediaRecorderService, MutationObserverService, NetworkInformationService, NotificationService, PageVisibilityService, PerformanceObserverService, PermissionsService, ResizeObserverService, ScreenOrientationService, ScreenWakeLockService, ServerSentEventsService, SpeechSynthesisService, VibrationService, WebAudioService, WebShareService, WebSocketClient, WebSocketService, WebStorageService, WebWorkerService, permissionGuard as createPermissionGuard, defaultBrowserWebApisConfig, injectGamepad, injectIntersectionObserver, injectMutationObserver, injectNetworkInformation, injectPageVisibility, injectPerformanceObserver, injectResizeObserver, injectScreenOrientation, permissionGuard, provideBattery, provideBroadcastChannel, provideBrowserWebApis, provideCamera, provideClipboard, provideCommunicationApis, provideFileSystemAccess, provideFullscreen, provideGamepad, provideGeolocation, provideIntersectionObserver, provideLocationApis, provideMediaApis, provideMediaDevices, provideMediaRecorder, provideMutationObserver, provideNetworkInformation, provideNotifications, providePageVisibility, providePerformanceObserver, providePermissions, provideResizeObserver, provideScreenOrientation, provideScreenWakeLock, provideServerSentEvents, provideSpeechSynthesis, provideStorageApis, provideVibration, provideWebAudio, provideWebShare, provideWebSocket, provideWebStorage, provideWebWorker, version };
|
|
3841
|
+
export { BROWSER_API_LOGGER, BatteryService, BroadcastChannelService, BrowserApiBaseService, BrowserCapabilityService, BrowserSupportUtil, CameraService, ClipboardService, ConnectionRegistryBaseService, FileSystemAccessService, FullscreenService, GamepadService, GeolocationService, IntersectionObserverService, MediaDevicesService, MediaRecorderService, MutationObserverService, NetworkInformationService, NotificationService, PageVisibilityService, PerformanceObserverService, PermissionsService, ResizeObserverService, ScreenOrientationService, ScreenWakeLockService, ServerSentEventsService, SpeechSynthesisService, VibrationService, WebAudioService, WebShareService, WebSocketClient, WebSocketService, WebStorageService, WebWorkerService, permissionGuard as createPermissionGuard, defaultBrowserWebApisConfig, injectBattery, injectClipboard, injectGamepad, injectGeolocation, injectIntersectionObserver, injectMutationObserver, injectNetworkInformation, injectPageVisibility, injectPerformanceObserver, injectResizeObserver, injectScreenOrientation, injectWakeLock, permissionGuard, provideBattery, provideBroadcastChannel, provideBrowserWebApis, provideCamera, provideClipboard, provideCommunicationApis, provideFileSystemAccess, provideFullscreen, provideGamepad, provideGeolocation, provideIntersectionObserver, provideLocationApis, provideMediaApis, provideMediaDevices, provideMediaRecorder, provideMutationObserver, provideNetworkInformation, provideNotifications, providePageVisibility, providePerformanceObserver, providePermissions, provideResizeObserver, provideScreenOrientation, provideScreenWakeLock, provideServerSentEvents, provideSpeechSynthesis, provideStorageApis, provideVibration, provideWebAudio, provideWebShare, provideWebSocket, provideWebStorage, provideWebWorker, version };
|