@angular-helpers/browser-web-apis 21.6.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,9 +1,9 @@
|
|
|
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
|
-
import {
|
|
6
|
-
import { filter,
|
|
5
|
+
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
|
+
import { filter, map, distinctUntilChanged } from 'rxjs/operators';
|
|
7
7
|
import { Router } from '@angular/router';
|
|
8
8
|
|
|
9
9
|
const BROWSER_API_LOGGER = new InjectionToken('BROWSER_API_LOGGER', {
|
|
@@ -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,6 +633,9 @@ 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) {
|
|
@@ -493,173 +669,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
493
669
|
type: Injectable
|
|
494
670
|
}] });
|
|
495
671
|
|
|
496
|
-
const BROWSER_CAPABILITIES = [
|
|
497
|
-
{ id: 'permissions', label: 'Permissions API', requiresSecureContext: false },
|
|
498
|
-
{ id: 'geolocation', label: 'Geolocation API', requiresSecureContext: true },
|
|
499
|
-
{ id: 'clipboard', label: 'Clipboard API', requiresSecureContext: true },
|
|
500
|
-
{ id: 'notification', label: 'Notification API', requiresSecureContext: true },
|
|
501
|
-
{ id: 'mediaDevices', label: 'MediaDevices API', requiresSecureContext: true },
|
|
502
|
-
{ id: 'camera', label: 'Camera API', requiresSecureContext: true },
|
|
503
|
-
{ id: 'webWorker', label: 'Web Worker API', requiresSecureContext: false },
|
|
504
|
-
{ id: 'regexSecurity', label: 'Regex Security', requiresSecureContext: false },
|
|
505
|
-
{ id: 'webStorage', label: 'Web Storage', requiresSecureContext: false },
|
|
506
|
-
{ id: 'webShare', label: 'Web Share', requiresSecureContext: true },
|
|
507
|
-
{ id: 'battery', label: 'Battery API', requiresSecureContext: false },
|
|
508
|
-
{ id: 'webSocket', label: 'WebSocket API', requiresSecureContext: false },
|
|
509
|
-
{ id: 'intersectionObserver', label: 'Intersection Observer', requiresSecureContext: false },
|
|
510
|
-
{ id: 'resizeObserver', label: 'Resize Observer', requiresSecureContext: false },
|
|
511
|
-
{ id: 'pageVisibility', label: 'Page Visibility API', requiresSecureContext: false },
|
|
512
|
-
{ id: 'broadcastChannel', label: 'Broadcast Channel API', requiresSecureContext: false },
|
|
513
|
-
{ id: 'networkInformation', label: 'Network Information API', requiresSecureContext: false },
|
|
514
|
-
{ id: 'screenWakeLock', label: 'Screen Wake Lock API', requiresSecureContext: true },
|
|
515
|
-
{ id: 'screenOrientation', label: 'Screen Orientation API', requiresSecureContext: false },
|
|
516
|
-
{ id: 'fullscreen', label: 'Fullscreen API', requiresSecureContext: false },
|
|
517
|
-
{ id: 'fileSystemAccess', label: 'File System Access API', requiresSecureContext: true },
|
|
518
|
-
{ id: 'mediaRecorder', label: 'MediaRecorder API', requiresSecureContext: true },
|
|
519
|
-
{ id: 'serverSentEvents', label: 'Server-Sent Events', requiresSecureContext: false },
|
|
520
|
-
{ id: 'vibration', label: 'Vibration API', requiresSecureContext: false },
|
|
521
|
-
{ id: 'speechSynthesis', label: 'Speech Synthesis API', requiresSecureContext: false },
|
|
522
|
-
{ id: 'mutationObserver', label: 'Mutation Observer', requiresSecureContext: false },
|
|
523
|
-
{ id: 'performanceObserver', label: 'Performance Observer', requiresSecureContext: false },
|
|
524
|
-
{ id: 'idleDetector', label: 'Idle Detection API', requiresSecureContext: true },
|
|
525
|
-
{ id: 'eyeDropper', label: 'EyeDropper API', requiresSecureContext: true },
|
|
526
|
-
{ id: 'barcodeDetector', label: 'Barcode Detection API', requiresSecureContext: true },
|
|
527
|
-
{ id: 'webAudio', label: 'Web Audio API', requiresSecureContext: false },
|
|
528
|
-
{ id: 'gamepad', label: 'Gamepad API', requiresSecureContext: true },
|
|
529
|
-
{ id: 'webBluetooth', label: 'Web Bluetooth API', requiresSecureContext: true },
|
|
530
|
-
{ id: 'webUsb', label: 'WebUSB API', requiresSecureContext: true },
|
|
531
|
-
{ id: 'webNfc', label: 'Web NFC API', requiresSecureContext: true },
|
|
532
|
-
{ id: 'paymentRequest', label: 'Payment Request API', requiresSecureContext: true },
|
|
533
|
-
{ id: 'credentialManagement', label: 'Credential Management API', requiresSecureContext: true },
|
|
534
|
-
];
|
|
535
|
-
class BrowserCapabilityService {
|
|
536
|
-
getCapabilities() {
|
|
537
|
-
return BROWSER_CAPABILITIES;
|
|
538
|
-
}
|
|
539
|
-
isSecureContext() {
|
|
540
|
-
return typeof window !== 'undefined' && window.isSecureContext;
|
|
541
|
-
}
|
|
542
|
-
isSupported(capability) {
|
|
543
|
-
switch (capability) {
|
|
544
|
-
case 'permissions':
|
|
545
|
-
return typeof navigator !== 'undefined' && 'permissions' in navigator;
|
|
546
|
-
case 'geolocation':
|
|
547
|
-
return typeof navigator !== 'undefined' && 'geolocation' in navigator;
|
|
548
|
-
case 'clipboard':
|
|
549
|
-
return typeof navigator !== 'undefined' && 'clipboard' in navigator;
|
|
550
|
-
case 'notification':
|
|
551
|
-
return typeof window !== 'undefined' && 'Notification' in window;
|
|
552
|
-
case 'mediaDevices':
|
|
553
|
-
case 'camera':
|
|
554
|
-
return typeof navigator !== 'undefined' && 'mediaDevices' in navigator;
|
|
555
|
-
case 'webWorker':
|
|
556
|
-
case 'regexSecurity':
|
|
557
|
-
return typeof Worker !== 'undefined';
|
|
558
|
-
case 'webStorage':
|
|
559
|
-
return typeof Storage !== 'undefined';
|
|
560
|
-
case 'webShare':
|
|
561
|
-
return typeof navigator !== 'undefined' && 'share' in navigator;
|
|
562
|
-
case 'battery':
|
|
563
|
-
return typeof navigator !== 'undefined' && 'getBattery' in navigator;
|
|
564
|
-
case 'webSocket':
|
|
565
|
-
return typeof WebSocket !== 'undefined';
|
|
566
|
-
case 'intersectionObserver':
|
|
567
|
-
return typeof IntersectionObserver !== 'undefined';
|
|
568
|
-
case 'resizeObserver':
|
|
569
|
-
return typeof ResizeObserver !== 'undefined';
|
|
570
|
-
case 'pageVisibility':
|
|
571
|
-
return typeof document !== 'undefined' && 'hidden' in document;
|
|
572
|
-
case 'broadcastChannel':
|
|
573
|
-
return typeof BroadcastChannel !== 'undefined';
|
|
574
|
-
case 'networkInformation':
|
|
575
|
-
return (typeof navigator !== 'undefined' &&
|
|
576
|
-
('connection' in navigator || 'mozConnection' in navigator));
|
|
577
|
-
case 'screenWakeLock':
|
|
578
|
-
return typeof navigator !== 'undefined' && 'wakeLock' in navigator;
|
|
579
|
-
case 'screenOrientation':
|
|
580
|
-
return typeof screen !== 'undefined' && 'orientation' in screen;
|
|
581
|
-
case 'fullscreen':
|
|
582
|
-
return (typeof document !== 'undefined' &&
|
|
583
|
-
('fullscreenEnabled' in document || 'webkitFullscreenEnabled' in document));
|
|
584
|
-
case 'fileSystemAccess':
|
|
585
|
-
return typeof window !== 'undefined' && 'showOpenFilePicker' in window;
|
|
586
|
-
case 'mediaRecorder':
|
|
587
|
-
return typeof MediaRecorder !== 'undefined';
|
|
588
|
-
case 'serverSentEvents':
|
|
589
|
-
return typeof EventSource !== 'undefined';
|
|
590
|
-
case 'vibration':
|
|
591
|
-
return typeof navigator !== 'undefined' && 'vibrate' in navigator;
|
|
592
|
-
case 'speechSynthesis':
|
|
593
|
-
return typeof window !== 'undefined' && 'speechSynthesis' in window;
|
|
594
|
-
case 'mutationObserver':
|
|
595
|
-
return typeof MutationObserver !== 'undefined';
|
|
596
|
-
case 'performanceObserver':
|
|
597
|
-
return typeof PerformanceObserver !== 'undefined';
|
|
598
|
-
case 'idleDetector':
|
|
599
|
-
return typeof window !== 'undefined' && 'IdleDetector' in window;
|
|
600
|
-
case 'eyeDropper':
|
|
601
|
-
return typeof window !== 'undefined' && 'EyeDropper' in window;
|
|
602
|
-
case 'barcodeDetector':
|
|
603
|
-
return typeof window !== 'undefined' && 'BarcodeDetector' in window;
|
|
604
|
-
case 'webAudio':
|
|
605
|
-
return typeof window !== 'undefined' && 'AudioContext' in window;
|
|
606
|
-
case 'gamepad':
|
|
607
|
-
return typeof navigator !== 'undefined' && 'getGamepads' in navigator;
|
|
608
|
-
case 'webBluetooth':
|
|
609
|
-
return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
|
|
610
|
-
case 'webUsb':
|
|
611
|
-
return typeof navigator !== 'undefined' && 'usb' in navigator;
|
|
612
|
-
case 'webNfc':
|
|
613
|
-
return typeof window !== 'undefined' && 'NDEFReader' in window;
|
|
614
|
-
case 'paymentRequest':
|
|
615
|
-
return typeof window !== 'undefined' && 'PaymentRequest' in window;
|
|
616
|
-
case 'credentialManagement':
|
|
617
|
-
return typeof navigator !== 'undefined' && 'credentials' in navigator;
|
|
618
|
-
default:
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
getAllStatuses() {
|
|
623
|
-
const secureContext = this.isSecureContext();
|
|
624
|
-
return this.getCapabilities().map((capability) => ({
|
|
625
|
-
id: capability.id,
|
|
626
|
-
label: capability.label,
|
|
627
|
-
supported: this.isSupported(capability.id),
|
|
628
|
-
secureContext,
|
|
629
|
-
requiresSecureContext: capability.requiresSecureContext,
|
|
630
|
-
}));
|
|
631
|
-
}
|
|
632
|
-
async getPermissionState(permission) {
|
|
633
|
-
if (typeof navigator === 'undefined' || !('permissions' in navigator)) {
|
|
634
|
-
return 'unknown';
|
|
635
|
-
}
|
|
636
|
-
try {
|
|
637
|
-
const status = await navigator.permissions.query({ name: permission });
|
|
638
|
-
return status.state;
|
|
639
|
-
}
|
|
640
|
-
catch {
|
|
641
|
-
return 'unknown';
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
645
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, providedIn: 'root' });
|
|
646
|
-
}
|
|
647
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BrowserCapabilityService, decorators: [{
|
|
648
|
-
type: Injectable,
|
|
649
|
-
args: [{ providedIn: 'root' }]
|
|
650
|
-
}] });
|
|
651
|
-
|
|
652
672
|
class BatteryService extends BrowserApiBaseService {
|
|
653
673
|
batteryManager = null;
|
|
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();
|
|
@@ -779,255 +792,337 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
779
792
|
type: Injectable
|
|
780
793
|
}] });
|
|
781
794
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
setupEventListeners() {
|
|
798
|
-
if (this.isBrowserEnvironment()) {
|
|
799
|
-
fromEvent(window, 'storage')
|
|
800
|
-
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
801
|
-
.subscribe((event) => {
|
|
802
|
-
const storageEvent = event;
|
|
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
|
-
});
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
serializeValue(value, options) {
|
|
814
|
-
if (options?.serialize) {
|
|
815
|
-
return options.serialize(value);
|
|
816
|
-
}
|
|
817
|
-
return JSON.stringify(value);
|
|
795
|
+
const SECURITY_WARN_KEY = Symbol('storage-security-warned');
|
|
796
|
+
/**
|
|
797
|
+
* Implementation of `StorageNamespace` shared by `local` and `session` namespaces.
|
|
798
|
+
* Wraps every native access in try/catch so Safari private mode and sandboxed iframes
|
|
799
|
+
* (which throw `SecurityError`) degrade gracefully instead of crashing the app.
|
|
800
|
+
*/
|
|
801
|
+
class StorageNamespaceImpl {
|
|
802
|
+
area;
|
|
803
|
+
events;
|
|
804
|
+
logger;
|
|
805
|
+
supportedCache = null;
|
|
806
|
+
constructor(area, events, logger) {
|
|
807
|
+
this.area = area;
|
|
808
|
+
this.events = events;
|
|
809
|
+
this.logger = logger;
|
|
818
810
|
}
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
return
|
|
822
|
-
if (
|
|
823
|
-
|
|
811
|
+
isSupported() {
|
|
812
|
+
if (this.supportedCache !== null)
|
|
813
|
+
return this.supportedCache;
|
|
814
|
+
if (typeof window === 'undefined' || typeof Storage === 'undefined') {
|
|
815
|
+
this.supportedCache = false;
|
|
816
|
+
return false;
|
|
824
817
|
}
|
|
825
818
|
try {
|
|
826
|
-
|
|
819
|
+
const store = this.getStore();
|
|
820
|
+
const probe = `__bwa_probe_${Date.now()}__`;
|
|
821
|
+
store.setItem(probe, '1');
|
|
822
|
+
store.removeItem(probe);
|
|
823
|
+
this.supportedCache = true;
|
|
827
824
|
}
|
|
828
825
|
catch {
|
|
829
|
-
|
|
826
|
+
this.warnSecurityOnce();
|
|
827
|
+
this.supportedCache = false;
|
|
830
828
|
}
|
|
829
|
+
return this.supportedCache;
|
|
831
830
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
836
|
-
emitStorageChange(fullKey, newValue, oldValue, area) {
|
|
837
|
-
this.storageEvents.set({ key: fullKey, newValue, oldValue, storageArea: area });
|
|
838
|
-
}
|
|
839
|
-
// Local Storage Methods
|
|
840
|
-
setLocalStorage(key, value, options = {}) {
|
|
841
|
-
this.ensureSupported();
|
|
831
|
+
set(key, value, opts = {}) {
|
|
832
|
+
if (!this.isSupported())
|
|
833
|
+
return false;
|
|
842
834
|
try {
|
|
843
|
-
const
|
|
844
|
-
const
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
this.
|
|
835
|
+
const fullKey = this.getKey(key, opts);
|
|
836
|
+
const oldRaw = this.getStore().getItem(fullKey);
|
|
837
|
+
const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, opts) : null;
|
|
838
|
+
const serialized = this.serializeValue(value, opts);
|
|
839
|
+
this.getStore().setItem(fullKey, serialized);
|
|
840
|
+
this.events.emit({ key: fullKey, newValue: value, oldValue, storageArea: this.area });
|
|
849
841
|
return true;
|
|
850
842
|
}
|
|
851
843
|
catch (error) {
|
|
852
|
-
this.
|
|
844
|
+
this.logger.error(`[storage:${this.area}] set("${key}") failed`, error);
|
|
853
845
|
return false;
|
|
854
846
|
}
|
|
855
847
|
}
|
|
856
|
-
|
|
857
|
-
this.
|
|
848
|
+
get(key, defaultValue = null, opts = {}) {
|
|
849
|
+
if (!this.isSupported())
|
|
850
|
+
return defaultValue;
|
|
858
851
|
try {
|
|
859
|
-
const fullKey = this.getKey(key,
|
|
860
|
-
const
|
|
861
|
-
return
|
|
852
|
+
const fullKey = this.getKey(key, opts);
|
|
853
|
+
const raw = this.getStore().getItem(fullKey);
|
|
854
|
+
return raw !== null ? this.deserializeValue(raw, opts) : defaultValue;
|
|
862
855
|
}
|
|
863
856
|
catch (error) {
|
|
864
|
-
this.
|
|
857
|
+
this.logger.error(`[storage:${this.area}] get("${key}") failed`, error);
|
|
865
858
|
return defaultValue;
|
|
866
859
|
}
|
|
867
860
|
}
|
|
868
|
-
|
|
869
|
-
this.
|
|
861
|
+
remove(key, opts = {}) {
|
|
862
|
+
if (!this.isSupported())
|
|
863
|
+
return false;
|
|
870
864
|
try {
|
|
871
|
-
const fullKey = this.getKey(key,
|
|
872
|
-
const oldRaw =
|
|
873
|
-
const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw,
|
|
874
|
-
|
|
875
|
-
this.
|
|
865
|
+
const fullKey = this.getKey(key, opts);
|
|
866
|
+
const oldRaw = this.getStore().getItem(fullKey);
|
|
867
|
+
const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, opts) : null;
|
|
868
|
+
this.getStore().removeItem(fullKey);
|
|
869
|
+
this.events.emit({ key: fullKey, newValue: null, oldValue, storageArea: this.area });
|
|
876
870
|
return true;
|
|
877
871
|
}
|
|
878
872
|
catch (error) {
|
|
879
|
-
this.
|
|
873
|
+
this.logger.error(`[storage:${this.area}] remove("${key}") failed`, error);
|
|
880
874
|
return false;
|
|
881
875
|
}
|
|
882
876
|
}
|
|
883
|
-
|
|
884
|
-
this.
|
|
877
|
+
clear(opts = {}) {
|
|
878
|
+
if (!this.isSupported())
|
|
879
|
+
return false;
|
|
885
880
|
try {
|
|
886
|
-
const prefix =
|
|
881
|
+
const prefix = opts?.prefix;
|
|
882
|
+
const store = this.getStore();
|
|
887
883
|
if (prefix) {
|
|
888
|
-
const
|
|
889
|
-
for (let i = 0; i <
|
|
890
|
-
const
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
884
|
+
const toRemove = [];
|
|
885
|
+
for (let i = 0; i < store.length; i++) {
|
|
886
|
+
const k = store.key(i);
|
|
887
|
+
if (k && k.startsWith(`${prefix}:`))
|
|
888
|
+
toRemove.push(k);
|
|
889
|
+
}
|
|
890
|
+
for (const k of toRemove) {
|
|
891
|
+
const oldRaw = store.getItem(k);
|
|
892
|
+
store.removeItem(k);
|
|
893
|
+
this.events.emit({
|
|
894
|
+
key: k,
|
|
895
|
+
newValue: null,
|
|
896
|
+
oldValue: oldRaw,
|
|
897
|
+
storageArea: this.area,
|
|
898
|
+
});
|
|
894
899
|
}
|
|
895
|
-
keysToRemove.forEach((key) => {
|
|
896
|
-
const oldRaw = localStorage.getItem(key);
|
|
897
|
-
localStorage.removeItem(key);
|
|
898
|
-
this.emitStorageChange(key, null, oldRaw, 'localStorage');
|
|
899
|
-
});
|
|
900
900
|
}
|
|
901
901
|
else {
|
|
902
|
-
|
|
903
|
-
this.
|
|
902
|
+
store.clear();
|
|
903
|
+
this.events.emit({ key: null, newValue: null, oldValue: null, storageArea: this.area });
|
|
904
904
|
}
|
|
905
905
|
return true;
|
|
906
906
|
}
|
|
907
907
|
catch (error) {
|
|
908
|
-
this.
|
|
908
|
+
this.logger.error(`[storage:${this.area}] clear() failed`, error);
|
|
909
909
|
return false;
|
|
910
910
|
}
|
|
911
911
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
912
|
+
size(opts = {}) {
|
|
913
|
+
if (!this.isSupported())
|
|
914
|
+
return 0;
|
|
915
915
|
try {
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
916
|
+
const store = this.getStore();
|
|
917
|
+
const prefix = opts?.prefix;
|
|
918
|
+
let total = 0;
|
|
919
|
+
for (let i = 0; i < store.length; i++) {
|
|
920
|
+
const k = store.key(i);
|
|
921
|
+
if (k && (!prefix || k.startsWith(`${prefix}:`))) {
|
|
922
|
+
total += (store.getItem(k)?.length ?? 0) + k.length;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return total;
|
|
923
926
|
}
|
|
924
927
|
catch (error) {
|
|
925
|
-
this.
|
|
926
|
-
return
|
|
928
|
+
this.logger.error(`[storage:${this.area}] size() failed`, error);
|
|
929
|
+
return 0;
|
|
927
930
|
}
|
|
928
931
|
}
|
|
929
|
-
|
|
930
|
-
this.
|
|
932
|
+
watch(key, opts = {}) {
|
|
933
|
+
const fullKey = this.getKey(key, opts);
|
|
934
|
+
return this.events.events$.pipe(filter((event) => event.storageArea === this.area && (event.key === null || event.key === fullKey)), map((event) => event.newValue));
|
|
935
|
+
}
|
|
936
|
+
native() {
|
|
937
|
+
if (!this.isSupported()) {
|
|
938
|
+
throw new Error(`${this.area} not supported in this environment`);
|
|
939
|
+
}
|
|
940
|
+
return this.getStore();
|
|
941
|
+
}
|
|
942
|
+
getStore() {
|
|
943
|
+
return this.area === 'localStorage' ? window.localStorage : window.sessionStorage;
|
|
944
|
+
}
|
|
945
|
+
getKey(key, opts) {
|
|
946
|
+
const prefix = opts?.prefix ?? '';
|
|
947
|
+
return prefix ? `${prefix}:${key}` : key;
|
|
948
|
+
}
|
|
949
|
+
serializeValue(value, opts) {
|
|
950
|
+
if (opts?.serialize)
|
|
951
|
+
return opts.serialize(value);
|
|
952
|
+
return JSON.stringify(value);
|
|
953
|
+
}
|
|
954
|
+
deserializeValue(value, opts) {
|
|
955
|
+
if (opts?.deserialize)
|
|
956
|
+
return opts.deserialize(value);
|
|
931
957
|
try {
|
|
932
|
-
|
|
933
|
-
const value = sessionStorage.getItem(fullKey);
|
|
934
|
-
return value !== null ? this.deserializeValue(value, options) : defaultValue;
|
|
958
|
+
return JSON.parse(value);
|
|
935
959
|
}
|
|
936
|
-
catch
|
|
937
|
-
|
|
938
|
-
return defaultValue;
|
|
960
|
+
catch {
|
|
961
|
+
return value;
|
|
939
962
|
}
|
|
940
963
|
}
|
|
941
|
-
|
|
942
|
-
|
|
964
|
+
warnSecurityOnce() {
|
|
965
|
+
const holder = globalThis;
|
|
966
|
+
if (!holder[SECURITY_WARN_KEY])
|
|
967
|
+
holder[SECURITY_WARN_KEY] = new Set();
|
|
968
|
+
if (holder[SECURITY_WARN_KEY].has(this.area))
|
|
969
|
+
return;
|
|
970
|
+
holder[SECURITY_WARN_KEY].add(this.area);
|
|
971
|
+
this.logger.warn(`[storage:${this.area}] access denied (SecurityError). Common causes: Safari private mode, ` +
|
|
972
|
+
'sandboxed iframes, or browser storage disabled. Falling back to default values.');
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
let legacyDeprecationLogged$1 = false;
|
|
977
|
+
/**
|
|
978
|
+
* Web Storage service with two namespaces (`local`, `session`) sharing one method
|
|
979
|
+
* surface. SecurityError-safe (Safari private mode, sandboxed iframes return defaults
|
|
980
|
+
* instead of throwing).
|
|
981
|
+
*
|
|
982
|
+
* Preferred usage:
|
|
983
|
+
* ```ts
|
|
984
|
+
* const storage = inject(WebStorageService);
|
|
985
|
+
* storage.local.set('user', { id: 1 });
|
|
986
|
+
* const user = storage.local.get<{ id: number }>('user');
|
|
987
|
+
* storage.local.watch<{ id: number }>('user').subscribe(console.log);
|
|
988
|
+
* ```
|
|
989
|
+
*
|
|
990
|
+
* Legacy methods (`setLocalStorage`, `getLocalStorage`, etc.) remain as deprecated
|
|
991
|
+
* wrappers for one minor cycle; removal slated for v22.
|
|
992
|
+
*/
|
|
993
|
+
class WebStorageService extends BrowserApiBaseService {
|
|
994
|
+
storageLogger = inject(BROWSER_API_LOGGER);
|
|
995
|
+
storageEvents = signal(null, ...(ngDevMode ? [{ debugName: "storageEvents" }] : /* istanbul ignore next */ []));
|
|
996
|
+
eventBus = {
|
|
997
|
+
emit: (event) => this.storageEvents.set(event),
|
|
998
|
+
events$: toObservable(this.storageEvents).pipe(filter((event) => event !== null), distinctUntilChanged((a, b) => a.key === b.key &&
|
|
999
|
+
a.newValue === b.newValue &&
|
|
1000
|
+
a.oldValue === b.oldValue &&
|
|
1001
|
+
a.storageArea === b.storageArea)),
|
|
1002
|
+
};
|
|
1003
|
+
/** Local storage namespace. */
|
|
1004
|
+
local = new StorageNamespaceImpl('localStorage', this.eventBus, this.storageLogger);
|
|
1005
|
+
/** Session storage namespace. */
|
|
1006
|
+
session = new StorageNamespaceImpl('sessionStorage', this.eventBus, this.storageLogger);
|
|
1007
|
+
constructor() {
|
|
1008
|
+
super();
|
|
1009
|
+
this.setupCrossTabListener();
|
|
1010
|
+
}
|
|
1011
|
+
getApiName() {
|
|
1012
|
+
return 'storage';
|
|
1013
|
+
}
|
|
1014
|
+
getCapabilityId() {
|
|
1015
|
+
return 'webStorage';
|
|
1016
|
+
}
|
|
1017
|
+
/** Returns true if either local or session storage is usable. */
|
|
1018
|
+
isSupported() {
|
|
1019
|
+
return this.local.isSupported() || this.session.isSupported();
|
|
1020
|
+
}
|
|
1021
|
+
/** Stream of every storage mutation observed in this tab or other tabs. */
|
|
1022
|
+
getStorageEvents() {
|
|
1023
|
+
return this.eventBus.events$;
|
|
1024
|
+
}
|
|
1025
|
+
setupCrossTabListener() {
|
|
1026
|
+
if (!this.isBrowserEnvironment())
|
|
1027
|
+
return;
|
|
1028
|
+
fromEvent(window, 'storage')
|
|
1029
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1030
|
+
.subscribe((event) => {
|
|
1031
|
+
const area = event.storageArea === window.localStorage ? 'localStorage' : 'sessionStorage';
|
|
1032
|
+
this.storageEvents.set({
|
|
1033
|
+
key: event.key,
|
|
1034
|
+
newValue: event.newValue ? this.safeParse(event.newValue) : null,
|
|
1035
|
+
oldValue: event.oldValue ? this.safeParse(event.oldValue) : null,
|
|
1036
|
+
storageArea: area,
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
safeParse(value) {
|
|
943
1041
|
try {
|
|
944
|
-
|
|
945
|
-
const oldRaw = sessionStorage.getItem(fullKey);
|
|
946
|
-
const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, options) : null;
|
|
947
|
-
sessionStorage.removeItem(fullKey);
|
|
948
|
-
this.emitStorageChange(fullKey, null, oldValue, 'sessionStorage');
|
|
949
|
-
return true;
|
|
1042
|
+
return JSON.parse(value);
|
|
950
1043
|
}
|
|
951
|
-
catch
|
|
952
|
-
|
|
953
|
-
return false;
|
|
1044
|
+
catch {
|
|
1045
|
+
return value;
|
|
954
1046
|
}
|
|
955
1047
|
}
|
|
1048
|
+
// ---------- legacy API (deprecated) ----------
|
|
1049
|
+
/** @deprecated Use `storage.local.set(key, value, opts)`. Removed in v22. */
|
|
1050
|
+
setLocalStorage(key, value, options = {}) {
|
|
1051
|
+
this.warnLegacyOnce();
|
|
1052
|
+
return this.local.set(key, value, options);
|
|
1053
|
+
}
|
|
1054
|
+
/** @deprecated Use `storage.local.get(key, defaultValue, opts)`. Removed in v22. */
|
|
1055
|
+
getLocalStorage(key, defaultValue = null, options = {}) {
|
|
1056
|
+
this.warnLegacyOnce();
|
|
1057
|
+
return this.local.get(key, defaultValue, options);
|
|
1058
|
+
}
|
|
1059
|
+
/** @deprecated Use `storage.local.remove(key, opts)`. Removed in v22. */
|
|
1060
|
+
removeLocalStorage(key, options = {}) {
|
|
1061
|
+
this.warnLegacyOnce();
|
|
1062
|
+
return this.local.remove(key, options);
|
|
1063
|
+
}
|
|
1064
|
+
/** @deprecated Use `storage.local.clear(opts)`. Removed in v22. */
|
|
1065
|
+
clearLocalStorage(options = {}) {
|
|
1066
|
+
this.warnLegacyOnce();
|
|
1067
|
+
return this.local.clear(options);
|
|
1068
|
+
}
|
|
1069
|
+
/** @deprecated Use `storage.session.set(key, value, opts)`. Removed in v22. */
|
|
1070
|
+
setSessionStorage(key, value, options = {}) {
|
|
1071
|
+
this.warnLegacyOnce();
|
|
1072
|
+
return this.session.set(key, value, options);
|
|
1073
|
+
}
|
|
1074
|
+
/** @deprecated Use `storage.session.get(key, defaultValue, opts)`. Removed in v22. */
|
|
1075
|
+
getSessionStorage(key, defaultValue = null, options = {}) {
|
|
1076
|
+
this.warnLegacyOnce();
|
|
1077
|
+
return this.session.get(key, defaultValue, options);
|
|
1078
|
+
}
|
|
1079
|
+
/** @deprecated Use `storage.session.remove(key, opts)`. Removed in v22. */
|
|
1080
|
+
removeSessionStorage(key, options = {}) {
|
|
1081
|
+
this.warnLegacyOnce();
|
|
1082
|
+
return this.session.remove(key, options);
|
|
1083
|
+
}
|
|
1084
|
+
/** @deprecated Use `storage.session.clear(opts)`. Removed in v22. */
|
|
956
1085
|
clearSessionStorage(options = {}) {
|
|
957
|
-
this.
|
|
958
|
-
|
|
959
|
-
const prefix = options?.prefix;
|
|
960
|
-
if (prefix) {
|
|
961
|
-
const keysToRemove = [];
|
|
962
|
-
for (let i = 0; i < sessionStorage.length; i++) {
|
|
963
|
-
const key = sessionStorage.key(i);
|
|
964
|
-
if (key && key.startsWith(`${prefix}:`)) {
|
|
965
|
-
keysToRemove.push(key);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
keysToRemove.forEach((key) => {
|
|
969
|
-
const oldRaw = sessionStorage.getItem(key);
|
|
970
|
-
sessionStorage.removeItem(key);
|
|
971
|
-
this.emitStorageChange(key, null, oldRaw, 'sessionStorage');
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
else {
|
|
975
|
-
sessionStorage.clear();
|
|
976
|
-
this.emitStorageChange(null, null, null, 'sessionStorage');
|
|
977
|
-
}
|
|
978
|
-
return true;
|
|
979
|
-
}
|
|
980
|
-
catch (error) {
|
|
981
|
-
this.logError('Error clearing sessionStorage:', error);
|
|
982
|
-
return false;
|
|
983
|
-
}
|
|
1086
|
+
this.warnLegacyOnce();
|
|
1087
|
+
return this.session.clear(options);
|
|
984
1088
|
}
|
|
985
|
-
|
|
1089
|
+
/** @deprecated Use `storage.local.size(opts)`. Removed in v22. */
|
|
986
1090
|
getLocalStorageSize(options = {}) {
|
|
987
|
-
this.
|
|
988
|
-
|
|
989
|
-
const prefix = options?.prefix;
|
|
990
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
991
|
-
const key = localStorage.key(i);
|
|
992
|
-
if (key && (!prefix || key.startsWith(`${prefix}:`))) {
|
|
993
|
-
totalSize += (localStorage.getItem(key)?.length || 0) + key.length;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return totalSize;
|
|
1091
|
+
this.warnLegacyOnce();
|
|
1092
|
+
return this.local.size(options);
|
|
997
1093
|
}
|
|
1094
|
+
/** @deprecated Use `storage.session.size(opts)`. Removed in v22. */
|
|
998
1095
|
getSessionStorageSize(options = {}) {
|
|
999
|
-
this.
|
|
1000
|
-
|
|
1001
|
-
const prefix = options?.prefix;
|
|
1002
|
-
for (let i = 0; i < sessionStorage.length; i++) {
|
|
1003
|
-
const key = sessionStorage.key(i);
|
|
1004
|
-
if (key && (!prefix || key.startsWith(`${prefix}:`))) {
|
|
1005
|
-
totalSize += (sessionStorage.getItem(key)?.length || 0) + key.length;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
return totalSize;
|
|
1009
|
-
}
|
|
1010
|
-
getStorageEvents() {
|
|
1011
|
-
return toObservable(this.storageEvents).pipe(filter((event) => event !== null), distinctUntilChanged((prev, curr) => prev.key === curr.key &&
|
|
1012
|
-
prev.newValue === curr.newValue &&
|
|
1013
|
-
prev.oldValue === curr.oldValue));
|
|
1096
|
+
this.warnLegacyOnce();
|
|
1097
|
+
return this.session.size(options);
|
|
1014
1098
|
}
|
|
1099
|
+
/** @deprecated Use `storage.local.watch<T>(key, opts)`. Removed in v22. */
|
|
1015
1100
|
watchLocalStorage(key, options = {}) {
|
|
1016
|
-
|
|
1017
|
-
return this.
|
|
1101
|
+
this.warnLegacyOnce();
|
|
1102
|
+
return this.local.watch(key, options);
|
|
1018
1103
|
}
|
|
1104
|
+
/** @deprecated Use `storage.session.watch<T>(key, opts)`. Removed in v22. */
|
|
1019
1105
|
watchSessionStorage(key, options = {}) {
|
|
1020
|
-
|
|
1021
|
-
return this.
|
|
1106
|
+
this.warnLegacyOnce();
|
|
1107
|
+
return this.session.watch(key, options);
|
|
1022
1108
|
}
|
|
1023
|
-
|
|
1109
|
+
/** @deprecated Use `storage.local.native()`. Removed in v22. */
|
|
1024
1110
|
getNativeLocalStorage() {
|
|
1025
|
-
this.
|
|
1026
|
-
return
|
|
1111
|
+
this.warnLegacyOnce();
|
|
1112
|
+
return this.local.native();
|
|
1027
1113
|
}
|
|
1114
|
+
/** @deprecated Use `storage.session.native()`. Removed in v22. */
|
|
1028
1115
|
getNativeSessionStorage() {
|
|
1029
|
-
this.
|
|
1030
|
-
return
|
|
1116
|
+
this.warnLegacyOnce();
|
|
1117
|
+
return this.session.native();
|
|
1118
|
+
}
|
|
1119
|
+
warnLegacyOnce() {
|
|
1120
|
+
if (legacyDeprecationLogged$1)
|
|
1121
|
+
return;
|
|
1122
|
+
legacyDeprecationLogged$1 = true;
|
|
1123
|
+
this.storageLogger.warn('[storage] WebStorageService.{set,get,remove,clear,watch}{Local,Session}Storage are ' +
|
|
1124
|
+
'deprecated. Use storage.local and storage.session namespaces. Legacy methods will be ' +
|
|
1125
|
+
'removed in v22.');
|
|
1031
1126
|
}
|
|
1032
1127
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1033
1128
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService });
|
|
@@ -1037,7 +1132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
1037
1132
|
}], ctorParameters: () => [] });
|
|
1038
1133
|
|
|
1039
1134
|
const DEFAULT_MAX_RECONNECT_DELAY = 30_000;
|
|
1040
|
-
const DEFAULT_REQUEST_TIMEOUT = 30_000;
|
|
1135
|
+
const DEFAULT_REQUEST_TIMEOUT$1 = 30_000;
|
|
1041
1136
|
/**
|
|
1042
1137
|
* Stateful WebSocket client wrapping a single connection. One instance per logical
|
|
1043
1138
|
* connection (do NOT share between `connect()` calls).
|
|
@@ -1109,7 +1204,7 @@ class WebSocketClient {
|
|
|
1109
1204
|
*/
|
|
1110
1205
|
request(type, data, opts) {
|
|
1111
1206
|
const id = this.generateId();
|
|
1112
|
-
const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
1207
|
+
const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT$1;
|
|
1113
1208
|
return new Promise((resolve, reject) => {
|
|
1114
1209
|
const timer = setTimeout(() => {
|
|
1115
1210
|
this.pendingRequests.delete(id);
|
|
@@ -1321,11 +1416,8 @@ class WebSocketService extends BrowserApiBaseService {
|
|
|
1321
1416
|
getApiName() {
|
|
1322
1417
|
return 'websocket';
|
|
1323
1418
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
if (typeof WebSocket === 'undefined') {
|
|
1327
|
-
throw new Error('WebSocket API not supported in this browser');
|
|
1328
|
-
}
|
|
1419
|
+
getCapabilityId() {
|
|
1420
|
+
return 'webSocket';
|
|
1329
1421
|
}
|
|
1330
1422
|
/**
|
|
1331
1423
|
* Create a new WebSocket client. The client owns one connection and is the recommended
|
|
@@ -1483,162 +1575,259 @@ function toObservableLike(client) {
|
|
|
1483
1575
|
});
|
|
1484
1576
|
}
|
|
1485
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
|
+
*/
|
|
1486
1592
|
class WebWorkerService extends BrowserApiBaseService {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
workerMessages = new Map();
|
|
1490
|
-
currentWorkerStatuses = new Map();
|
|
1593
|
+
workerLogger = inject(BROWSER_API_LOGGER);
|
|
1594
|
+
entries = new Map();
|
|
1491
1595
|
_cleanup = this.destroyRef.onDestroy(() => this.terminateAllWorkers());
|
|
1492
1596
|
getApiName() {
|
|
1493
1597
|
return 'webworker';
|
|
1494
1598
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
if (typeof Worker === 'undefined') {
|
|
1498
|
-
throw new Error('Web Workers not supported in this browser');
|
|
1499
|
-
}
|
|
1599
|
+
getCapabilityId() {
|
|
1600
|
+
return 'webWorker';
|
|
1500
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
|
+
*/
|
|
1501
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) {
|
|
1502
1618
|
this.ensureSupported();
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
this.logError(`Failed to create worker ${name}:`, error);
|
|
1525
|
-
const status = {
|
|
1526
|
-
initialized: false,
|
|
1527
|
-
running: false,
|
|
1528
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1529
|
-
messageCount: 0,
|
|
1530
|
-
};
|
|
1531
|
-
this.currentWorkerStatuses.set(name, status);
|
|
1532
|
-
this.updateWorkerStatus(name, status);
|
|
1533
|
-
observer.next(status);
|
|
1534
|
-
}
|
|
1535
|
-
return () => {
|
|
1536
|
-
// No-op: workers are managed explicitly via terminateWorker/terminateAllWorkers
|
|
1537
|
-
};
|
|
1538
|
-
});
|
|
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();
|
|
1539
1640
|
}
|
|
1540
1641
|
terminateWorker(name) {
|
|
1541
|
-
const
|
|
1542
|
-
if (
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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);
|
|
1549
1650
|
}
|
|
1550
1651
|
terminateAllWorkers() {
|
|
1551
|
-
this.
|
|
1652
|
+
for (const name of [...this.entries.keys()]) {
|
|
1552
1653
|
this.terminateWorker(name);
|
|
1553
|
-
}
|
|
1654
|
+
}
|
|
1554
1655
|
}
|
|
1656
|
+
/** Send a fire-and-forget message. Use {@link request} when you need a reply. */
|
|
1555
1657
|
postMessage(workerName, task) {
|
|
1556
|
-
const
|
|
1557
|
-
if (!worker) {
|
|
1558
|
-
this.
|
|
1658
|
+
const entry = this.entries.get(workerName);
|
|
1659
|
+
if (!entry || !entry.worker) {
|
|
1660
|
+
this.workerLogger.error(`[webworker] postMessage: worker "${workerName}" not found`);
|
|
1559
1661
|
return;
|
|
1560
1662
|
}
|
|
1663
|
+
const message = {
|
|
1664
|
+
id: task.id ?? this.generateId(),
|
|
1665
|
+
type: task.type,
|
|
1666
|
+
data: task.data,
|
|
1667
|
+
timestamp: Date.now(),
|
|
1668
|
+
};
|
|
1561
1669
|
try {
|
|
1562
|
-
const message = { ...task, timestamp: Date.now() };
|
|
1563
1670
|
if (task.transferable) {
|
|
1564
|
-
worker.postMessage(message, task.transferable);
|
|
1671
|
+
entry.worker.postMessage(message, task.transferable);
|
|
1565
1672
|
}
|
|
1566
1673
|
else {
|
|
1567
|
-
worker.postMessage(message);
|
|
1568
|
-
}
|
|
1569
|
-
const currentStatus = this.currentWorkerStatuses.get(workerName);
|
|
1570
|
-
if (currentStatus) {
|
|
1571
|
-
currentStatus.messageCount++;
|
|
1572
|
-
this.updateWorkerStatus(workerName, currentStatus);
|
|
1674
|
+
entry.worker.postMessage(message);
|
|
1573
1675
|
}
|
|
1676
|
+
this.bumpMessageCount(entry);
|
|
1574
1677
|
}
|
|
1575
1678
|
catch (error) {
|
|
1576
|
-
this.
|
|
1679
|
+
this.workerLogger.error(`[webworker] postMessage failed for "${workerName}"`, error);
|
|
1577
1680
|
}
|
|
1578
1681
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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`));
|
|
1582
1694
|
}
|
|
1583
|
-
|
|
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));
|
|
1584
1736
|
}
|
|
1737
|
+
/** @deprecated Use {@link getStatusSignal}. Kept as Observable for backward compat. */
|
|
1585
1738
|
getStatus(workerName) {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
return this.
|
|
1739
|
+
return toObservable(this.getStatusSignal(workerName));
|
|
1740
|
+
}
|
|
1741
|
+
getStatusSignal(workerName) {
|
|
1742
|
+
return this.ensureEntry(workerName).status.asReadonly();
|
|
1590
1743
|
}
|
|
1591
1744
|
getCurrentStatus(workerName) {
|
|
1592
|
-
return this.
|
|
1745
|
+
return this.entries.get(workerName)?.status();
|
|
1593
1746
|
}
|
|
1594
1747
|
getAllStatuses() {
|
|
1595
|
-
|
|
1748
|
+
const result = new Map();
|
|
1749
|
+
for (const [name, entry] of this.entries)
|
|
1750
|
+
result.set(name, entry.status());
|
|
1751
|
+
return result;
|
|
1596
1752
|
}
|
|
1597
1753
|
isWorkerRunning(workerName) {
|
|
1598
|
-
|
|
1599
|
-
|
|
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;
|
|
1600
1766
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
+
}
|
|
1603
1781
|
const message = {
|
|
1604
|
-
id:
|
|
1605
|
-
type:
|
|
1606
|
-
data:
|
|
1607
|
-
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,
|
|
1608
1787
|
};
|
|
1609
|
-
|
|
1610
|
-
this.workerMessages.set(name, new Subject());
|
|
1611
|
-
}
|
|
1612
|
-
this.workerMessages.get(name).next(message);
|
|
1788
|
+
entry.messages$.next(message);
|
|
1613
1789
|
};
|
|
1614
|
-
worker.onerror = (error) => {
|
|
1615
|
-
this.
|
|
1616
|
-
|
|
1617
|
-
|
|
1790
|
+
entry.worker.onerror = (error) => {
|
|
1791
|
+
this.workerLogger.error(`[webworker] Worker "${name}" error`, error);
|
|
1792
|
+
entry.status.update((s) => ({
|
|
1793
|
+
...s,
|
|
1618
1794
|
running: false,
|
|
1619
1795
|
error: error instanceof Error ? error.message : 'Worker error',
|
|
1620
|
-
|
|
1621
|
-
};
|
|
1622
|
-
this.currentWorkerStatuses.set(name, status);
|
|
1623
|
-
this.updateWorkerStatus(name, status);
|
|
1796
|
+
}));
|
|
1624
1797
|
};
|
|
1625
|
-
// Auto-cleanup when service is destroyed
|
|
1626
|
-
this.destroyRef.onDestroy(() => {
|
|
1627
|
-
this.terminateWorker(name);
|
|
1628
|
-
});
|
|
1629
1798
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1799
|
+
rejectPending(entry, reason) {
|
|
1800
|
+
for (const p of entry.pending.values()) {
|
|
1801
|
+
clearTimeout(p.timer);
|
|
1802
|
+
p.reject(reason);
|
|
1633
1803
|
}
|
|
1634
|
-
|
|
1804
|
+
entry.pending.clear();
|
|
1635
1805
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
return this.workers.get(name);
|
|
1806
|
+
bumpMessageCount(entry) {
|
|
1807
|
+
entry.status.update((s) => ({ ...s, messageCount: s.messageCount + 1 }));
|
|
1639
1808
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
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)}`;
|
|
1642
1831
|
}
|
|
1643
1832
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1644
1833
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebWorkerService });
|
|
@@ -1678,8 +1867,8 @@ class IntersectionObserverService extends BrowserApiBaseService {
|
|
|
1678
1867
|
getApiName() {
|
|
1679
1868
|
return 'intersection-observer';
|
|
1680
1869
|
}
|
|
1681
|
-
|
|
1682
|
-
return
|
|
1870
|
+
getCapabilityId() {
|
|
1871
|
+
return 'intersectionObserver';
|
|
1683
1872
|
}
|
|
1684
1873
|
observe(element, options = {}) {
|
|
1685
1874
|
if (!this.isSupported()) {
|
|
@@ -1738,8 +1927,8 @@ class ResizeObserverService extends BrowserApiBaseService {
|
|
|
1738
1927
|
getApiName() {
|
|
1739
1928
|
return 'resize-observer';
|
|
1740
1929
|
}
|
|
1741
|
-
|
|
1742
|
-
return
|
|
1930
|
+
getCapabilityId() {
|
|
1931
|
+
return 'resizeObserver';
|
|
1743
1932
|
}
|
|
1744
1933
|
observe(element, options = {}) {
|
|
1745
1934
|
if (!this.isSupported()) {
|
|
@@ -1779,8 +1968,8 @@ class PageVisibilityService extends BrowserApiBaseService {
|
|
|
1779
1968
|
getApiName() {
|
|
1780
1969
|
return 'page-visibility';
|
|
1781
1970
|
}
|
|
1782
|
-
|
|
1783
|
-
return
|
|
1971
|
+
getCapabilityId() {
|
|
1972
|
+
return 'pageVisibility';
|
|
1784
1973
|
}
|
|
1785
1974
|
get isHidden() {
|
|
1786
1975
|
if (!this.isSupported())
|
|
@@ -1856,13 +2045,11 @@ class BroadcastChannelService extends ConnectionRegistryBaseService {
|
|
|
1856
2045
|
closeNativeConnection(channel) {
|
|
1857
2046
|
channel.close();
|
|
1858
2047
|
}
|
|
1859
|
-
|
|
1860
|
-
return
|
|
2048
|
+
getCapabilityId() {
|
|
2049
|
+
return 'broadcastChannel';
|
|
1861
2050
|
}
|
|
1862
2051
|
ensureBroadcastChannelSupported() {
|
|
1863
|
-
|
|
1864
|
-
throw new Error('BroadcastChannel API not supported in this environment');
|
|
1865
|
-
}
|
|
2052
|
+
this.ensureSupported();
|
|
1866
2053
|
}
|
|
1867
2054
|
open(name) {
|
|
1868
2055
|
this.ensureBroadcastChannelSupported();
|
|
@@ -1959,8 +2146,8 @@ class NetworkInformationService extends BrowserApiBaseService {
|
|
|
1959
2146
|
getApiName() {
|
|
1960
2147
|
return 'network-information';
|
|
1961
2148
|
}
|
|
1962
|
-
|
|
1963
|
-
return
|
|
2149
|
+
getCapabilityId() {
|
|
2150
|
+
return 'networkInformation';
|
|
1964
2151
|
}
|
|
1965
2152
|
getSnapshot() {
|
|
1966
2153
|
return this.isBrowserEnvironment() ? getNetworkSnapshot() : { online: true };
|
|
@@ -1983,8 +2170,8 @@ class ScreenWakeLockService extends BrowserApiBaseService {
|
|
|
1983
2170
|
getApiName() {
|
|
1984
2171
|
return 'screen-wake-lock';
|
|
1985
2172
|
}
|
|
1986
|
-
|
|
1987
|
-
return
|
|
2173
|
+
getCapabilityId() {
|
|
2174
|
+
return 'screenWakeLock';
|
|
1988
2175
|
}
|
|
1989
2176
|
get isActive() {
|
|
1990
2177
|
return this.sentinel !== null && !this.sentinel.released;
|
|
@@ -2078,8 +2265,8 @@ class ScreenOrientationService extends BrowserApiBaseService {
|
|
|
2078
2265
|
getApiName() {
|
|
2079
2266
|
return 'screen-orientation';
|
|
2080
2267
|
}
|
|
2081
|
-
|
|
2082
|
-
return
|
|
2268
|
+
getCapabilityId() {
|
|
2269
|
+
return 'screenOrientation';
|
|
2083
2270
|
}
|
|
2084
2271
|
getSnapshot() {
|
|
2085
2272
|
return this.isBrowserEnvironment()
|
|
@@ -2123,8 +2310,12 @@ class FullscreenService extends BrowserApiBaseService {
|
|
|
2123
2310
|
getApiName() {
|
|
2124
2311
|
return 'fullscreen';
|
|
2125
2312
|
}
|
|
2313
|
+
getCapabilityId() {
|
|
2314
|
+
return 'fullscreen';
|
|
2315
|
+
}
|
|
2316
|
+
/** Override to also check the *enabled* flag (browser may have disabled fullscreen). */
|
|
2126
2317
|
isSupported() {
|
|
2127
|
-
if (!
|
|
2318
|
+
if (!super.isSupported())
|
|
2128
2319
|
return false;
|
|
2129
2320
|
return !!(document.fullscreenEnabled ??
|
|
2130
2321
|
document.webkitFullscreenEnabled);
|
|
@@ -2215,17 +2406,18 @@ class FileSystemAccessService extends BrowserApiBaseService {
|
|
|
2215
2406
|
getApiName() {
|
|
2216
2407
|
return 'file-system-access';
|
|
2217
2408
|
}
|
|
2409
|
+
getCapabilityId() {
|
|
2410
|
+
return 'fileSystemAccess';
|
|
2411
|
+
}
|
|
2412
|
+
/** Override to also assert secure context (required by the spec). */
|
|
2218
2413
|
isSupported() {
|
|
2219
|
-
return
|
|
2414
|
+
return super.isSupported() && typeof window !== 'undefined' && window.isSecureContext;
|
|
2220
2415
|
}
|
|
2221
2416
|
get win() {
|
|
2222
2417
|
return window;
|
|
2223
2418
|
}
|
|
2224
2419
|
ensureSupported() {
|
|
2225
2420
|
super.ensureSupported();
|
|
2226
|
-
if (!('showOpenFilePicker' in window)) {
|
|
2227
|
-
throw new Error('File System Access API not supported in this browser');
|
|
2228
|
-
}
|
|
2229
2421
|
if (!window.isSecureContext) {
|
|
2230
2422
|
throw new Error('File System Access API requires a secure context (HTTPS)');
|
|
2231
2423
|
}
|
|
@@ -2301,8 +2493,8 @@ class MediaRecorderService extends BrowserApiBaseService {
|
|
|
2301
2493
|
getApiName() {
|
|
2302
2494
|
return 'media-recorder';
|
|
2303
2495
|
}
|
|
2304
|
-
|
|
2305
|
-
return
|
|
2496
|
+
getCapabilityId() {
|
|
2497
|
+
return 'mediaRecorder';
|
|
2306
2498
|
}
|
|
2307
2499
|
get state() {
|
|
2308
2500
|
return this.recorder?.state ?? 'inactive';
|
|
@@ -2392,13 +2584,11 @@ class ServerSentEventsService extends ConnectionRegistryBaseService {
|
|
|
2392
2584
|
closeNativeConnection(source) {
|
|
2393
2585
|
source.close();
|
|
2394
2586
|
}
|
|
2395
|
-
|
|
2396
|
-
return
|
|
2587
|
+
getCapabilityId() {
|
|
2588
|
+
return 'serverSentEvents';
|
|
2397
2589
|
}
|
|
2398
2590
|
ensureSSESupported() {
|
|
2399
|
-
|
|
2400
|
-
throw new Error('Server-Sent Events (EventSource) not supported in this environment');
|
|
2401
|
-
}
|
|
2591
|
+
this.ensureSupported();
|
|
2402
2592
|
}
|
|
2403
2593
|
connect(url, config = {}) {
|
|
2404
2594
|
this.ensureSSESupported();
|
|
@@ -2478,8 +2668,8 @@ class VibrationService extends BrowserApiBaseService {
|
|
|
2478
2668
|
notification: [200],
|
|
2479
2669
|
doubleTap: [50, 100, 50],
|
|
2480
2670
|
};
|
|
2481
|
-
|
|
2482
|
-
return
|
|
2671
|
+
getCapabilityId() {
|
|
2672
|
+
return 'vibration';
|
|
2483
2673
|
}
|
|
2484
2674
|
vibrate(pattern = 200) {
|
|
2485
2675
|
if (!this.isSupported())
|
|
@@ -2512,13 +2702,11 @@ class SpeechSynthesisService extends BrowserApiBaseService {
|
|
|
2512
2702
|
getApiName() {
|
|
2513
2703
|
return 'speech-synthesis';
|
|
2514
2704
|
}
|
|
2515
|
-
|
|
2516
|
-
return
|
|
2705
|
+
getCapabilityId() {
|
|
2706
|
+
return 'speechSynthesis';
|
|
2517
2707
|
}
|
|
2518
2708
|
ensureSpeechSynthesisSupported() {
|
|
2519
|
-
|
|
2520
|
-
throw new Error('Speech Synthesis API not supported in this browser');
|
|
2521
|
-
}
|
|
2709
|
+
this.ensureSupported();
|
|
2522
2710
|
}
|
|
2523
2711
|
get state() {
|
|
2524
2712
|
if (!this.isSupported())
|
|
@@ -2628,8 +2816,8 @@ class MutationObserverService extends BrowserApiBaseService {
|
|
|
2628
2816
|
getApiName() {
|
|
2629
2817
|
return 'mutation-observer';
|
|
2630
2818
|
}
|
|
2631
|
-
|
|
2632
|
-
return
|
|
2819
|
+
getCapabilityId() {
|
|
2820
|
+
return 'mutationObserver';
|
|
2633
2821
|
}
|
|
2634
2822
|
observe(target, options) {
|
|
2635
2823
|
if (!this.isSupported()) {
|
|
@@ -2666,8 +2854,8 @@ class PerformanceObserverService extends BrowserApiBaseService {
|
|
|
2666
2854
|
getApiName() {
|
|
2667
2855
|
return 'performance-observer';
|
|
2668
2856
|
}
|
|
2669
|
-
|
|
2670
|
-
return
|
|
2857
|
+
getCapabilityId() {
|
|
2858
|
+
return 'performanceObserver';
|
|
2671
2859
|
}
|
|
2672
2860
|
observe(config) {
|
|
2673
2861
|
if (!this.isSupported()) {
|
|
@@ -2695,8 +2883,8 @@ class WebAudioService extends BrowserApiBaseService {
|
|
|
2695
2883
|
return 'web-audio';
|
|
2696
2884
|
}
|
|
2697
2885
|
context = null;
|
|
2698
|
-
|
|
2699
|
-
return
|
|
2886
|
+
getCapabilityId() {
|
|
2887
|
+
return 'webAudio';
|
|
2700
2888
|
}
|
|
2701
2889
|
getContext() {
|
|
2702
2890
|
if (!this.isSupported()) {
|
|
@@ -2857,8 +3045,8 @@ class GamepadService extends BrowserApiBaseService {
|
|
|
2857
3045
|
getApiName() {
|
|
2858
3046
|
return 'gamepad';
|
|
2859
3047
|
}
|
|
2860
|
-
|
|
2861
|
-
return
|
|
3048
|
+
getCapabilityId() {
|
|
3049
|
+
return 'gamepad';
|
|
2862
3050
|
}
|
|
2863
3051
|
getSnapshot(index) {
|
|
2864
3052
|
if (!this.isSupported())
|
|
@@ -3078,6 +3266,257 @@ function injectGamepad(index, intervalMs = 16) {
|
|
|
3078
3266
|
};
|
|
3079
3267
|
}
|
|
3080
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
|
+
|
|
3081
3520
|
class BrowserSupportUtil {
|
|
3082
3521
|
static isSupported(feature) {
|
|
3083
3522
|
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
@@ -3399,4 +3838,4 @@ const version = '0.1.0';
|
|
|
3399
3838
|
* Generated bundle index. Do not edit.
|
|
3400
3839
|
*/
|
|
3401
3840
|
|
|
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 };
|
|
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 };
|