@angular-helpers/browser-web-apis 21.5.0 → 21.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/angular-helpers-browser-web-apis-experimental.mjs +478 -0
- package/fesm2022/angular-helpers-browser-web-apis.mjs +671 -816
- package/package.json +5 -1
- package/types/angular-helpers-browser-web-apis-experimental.d.ts +325 -0
- package/types/angular-helpers-browser-web-apis.d.ts +205 -329
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, inject, DestroyRef, PLATFORM_ID, Injectable, signal, computed, isSignal, effect, ElementRef, makeEnvironmentProviders } from '@angular/core';
|
|
3
3
|
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
|
|
4
|
-
import { Observable, fromEvent,
|
|
5
|
-
import {
|
|
6
|
-
import { filter,
|
|
4
|
+
import { Observable, fromEvent, Subject, of, map as map$1 } from 'rxjs';
|
|
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', {
|
|
@@ -779,255 +779,340 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
779
779
|
type: Injectable
|
|
780
780
|
}] });
|
|
781
781
|
|
|
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);
|
|
782
|
+
const SECURITY_WARN_KEY = Symbol('storage-security-warned');
|
|
783
|
+
/**
|
|
784
|
+
* Implementation of `StorageNamespace` shared by `local` and `session` namespaces.
|
|
785
|
+
* Wraps every native access in try/catch so Safari private mode and sandboxed iframes
|
|
786
|
+
* (which throw `SecurityError`) degrade gracefully instead of crashing the app.
|
|
787
|
+
*/
|
|
788
|
+
class StorageNamespaceImpl {
|
|
789
|
+
area;
|
|
790
|
+
events;
|
|
791
|
+
logger;
|
|
792
|
+
supportedCache = null;
|
|
793
|
+
constructor(area, events, logger) {
|
|
794
|
+
this.area = area;
|
|
795
|
+
this.events = events;
|
|
796
|
+
this.logger = logger;
|
|
818
797
|
}
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
return
|
|
822
|
-
if (
|
|
823
|
-
|
|
798
|
+
isSupported() {
|
|
799
|
+
if (this.supportedCache !== null)
|
|
800
|
+
return this.supportedCache;
|
|
801
|
+
if (typeof window === 'undefined' || typeof Storage === 'undefined') {
|
|
802
|
+
this.supportedCache = false;
|
|
803
|
+
return false;
|
|
824
804
|
}
|
|
825
805
|
try {
|
|
826
|
-
|
|
806
|
+
const store = this.getStore();
|
|
807
|
+
const probe = `__bwa_probe_${Date.now()}__`;
|
|
808
|
+
store.setItem(probe, '1');
|
|
809
|
+
store.removeItem(probe);
|
|
810
|
+
this.supportedCache = true;
|
|
827
811
|
}
|
|
828
812
|
catch {
|
|
829
|
-
|
|
813
|
+
this.warnSecurityOnce();
|
|
814
|
+
this.supportedCache = false;
|
|
830
815
|
}
|
|
816
|
+
return this.supportedCache;
|
|
831
817
|
}
|
|
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();
|
|
818
|
+
set(key, value, opts = {}) {
|
|
819
|
+
if (!this.isSupported())
|
|
820
|
+
return false;
|
|
842
821
|
try {
|
|
843
|
-
const
|
|
844
|
-
const
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
this.
|
|
822
|
+
const fullKey = this.getKey(key, opts);
|
|
823
|
+
const oldRaw = this.getStore().getItem(fullKey);
|
|
824
|
+
const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, opts) : null;
|
|
825
|
+
const serialized = this.serializeValue(value, opts);
|
|
826
|
+
this.getStore().setItem(fullKey, serialized);
|
|
827
|
+
this.events.emit({ key: fullKey, newValue: value, oldValue, storageArea: this.area });
|
|
849
828
|
return true;
|
|
850
829
|
}
|
|
851
830
|
catch (error) {
|
|
852
|
-
this.
|
|
831
|
+
this.logger.error(`[storage:${this.area}] set("${key}") failed`, error);
|
|
853
832
|
return false;
|
|
854
833
|
}
|
|
855
834
|
}
|
|
856
|
-
|
|
857
|
-
this.
|
|
835
|
+
get(key, defaultValue = null, opts = {}) {
|
|
836
|
+
if (!this.isSupported())
|
|
837
|
+
return defaultValue;
|
|
858
838
|
try {
|
|
859
|
-
const fullKey = this.getKey(key,
|
|
860
|
-
const
|
|
861
|
-
return
|
|
839
|
+
const fullKey = this.getKey(key, opts);
|
|
840
|
+
const raw = this.getStore().getItem(fullKey);
|
|
841
|
+
return raw !== null ? this.deserializeValue(raw, opts) : defaultValue;
|
|
862
842
|
}
|
|
863
843
|
catch (error) {
|
|
864
|
-
this.
|
|
844
|
+
this.logger.error(`[storage:${this.area}] get("${key}") failed`, error);
|
|
865
845
|
return defaultValue;
|
|
866
846
|
}
|
|
867
847
|
}
|
|
868
|
-
|
|
869
|
-
this.
|
|
848
|
+
remove(key, opts = {}) {
|
|
849
|
+
if (!this.isSupported())
|
|
850
|
+
return false;
|
|
870
851
|
try {
|
|
871
|
-
const fullKey = this.getKey(key,
|
|
872
|
-
const oldRaw =
|
|
873
|
-
const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw,
|
|
874
|
-
|
|
875
|
-
this.
|
|
852
|
+
const fullKey = this.getKey(key, opts);
|
|
853
|
+
const oldRaw = this.getStore().getItem(fullKey);
|
|
854
|
+
const oldValue = oldRaw !== null ? this.deserializeValue(oldRaw, opts) : null;
|
|
855
|
+
this.getStore().removeItem(fullKey);
|
|
856
|
+
this.events.emit({ key: fullKey, newValue: null, oldValue, storageArea: this.area });
|
|
876
857
|
return true;
|
|
877
858
|
}
|
|
878
859
|
catch (error) {
|
|
879
|
-
this.
|
|
860
|
+
this.logger.error(`[storage:${this.area}] remove("${key}") failed`, error);
|
|
880
861
|
return false;
|
|
881
862
|
}
|
|
882
863
|
}
|
|
883
|
-
|
|
884
|
-
this.
|
|
864
|
+
clear(opts = {}) {
|
|
865
|
+
if (!this.isSupported())
|
|
866
|
+
return false;
|
|
885
867
|
try {
|
|
886
|
-
const prefix =
|
|
868
|
+
const prefix = opts?.prefix;
|
|
869
|
+
const store = this.getStore();
|
|
887
870
|
if (prefix) {
|
|
888
|
-
const
|
|
889
|
-
for (let i = 0; i <
|
|
890
|
-
const
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
871
|
+
const toRemove = [];
|
|
872
|
+
for (let i = 0; i < store.length; i++) {
|
|
873
|
+
const k = store.key(i);
|
|
874
|
+
if (k && k.startsWith(`${prefix}:`))
|
|
875
|
+
toRemove.push(k);
|
|
876
|
+
}
|
|
877
|
+
for (const k of toRemove) {
|
|
878
|
+
const oldRaw = store.getItem(k);
|
|
879
|
+
store.removeItem(k);
|
|
880
|
+
this.events.emit({
|
|
881
|
+
key: k,
|
|
882
|
+
newValue: null,
|
|
883
|
+
oldValue: oldRaw,
|
|
884
|
+
storageArea: this.area,
|
|
885
|
+
});
|
|
894
886
|
}
|
|
895
|
-
keysToRemove.forEach((key) => {
|
|
896
|
-
const oldRaw = localStorage.getItem(key);
|
|
897
|
-
localStorage.removeItem(key);
|
|
898
|
-
this.emitStorageChange(key, null, oldRaw, 'localStorage');
|
|
899
|
-
});
|
|
900
887
|
}
|
|
901
888
|
else {
|
|
902
|
-
|
|
903
|
-
this.
|
|
889
|
+
store.clear();
|
|
890
|
+
this.events.emit({ key: null, newValue: null, oldValue: null, storageArea: this.area });
|
|
904
891
|
}
|
|
905
892
|
return true;
|
|
906
893
|
}
|
|
907
894
|
catch (error) {
|
|
908
|
-
this.
|
|
895
|
+
this.logger.error(`[storage:${this.area}] clear() failed`, error);
|
|
909
896
|
return false;
|
|
910
897
|
}
|
|
911
898
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
899
|
+
size(opts = {}) {
|
|
900
|
+
if (!this.isSupported())
|
|
901
|
+
return 0;
|
|
915
902
|
try {
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
903
|
+
const store = this.getStore();
|
|
904
|
+
const prefix = opts?.prefix;
|
|
905
|
+
let total = 0;
|
|
906
|
+
for (let i = 0; i < store.length; i++) {
|
|
907
|
+
const k = store.key(i);
|
|
908
|
+
if (k && (!prefix || k.startsWith(`${prefix}:`))) {
|
|
909
|
+
total += (store.getItem(k)?.length ?? 0) + k.length;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return total;
|
|
923
913
|
}
|
|
924
914
|
catch (error) {
|
|
925
|
-
this.
|
|
926
|
-
return
|
|
915
|
+
this.logger.error(`[storage:${this.area}] size() failed`, error);
|
|
916
|
+
return 0;
|
|
927
917
|
}
|
|
928
918
|
}
|
|
929
|
-
|
|
930
|
-
this.
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
catch (error) {
|
|
937
|
-
this.logError('Error getting sessionStorage:', error);
|
|
938
|
-
return defaultValue;
|
|
919
|
+
watch(key, opts = {}) {
|
|
920
|
+
const fullKey = this.getKey(key, opts);
|
|
921
|
+
return this.events.events$.pipe(filter((event) => event.storageArea === this.area && (event.key === null || event.key === fullKey)), map((event) => event.newValue));
|
|
922
|
+
}
|
|
923
|
+
native() {
|
|
924
|
+
if (!this.isSupported()) {
|
|
925
|
+
throw new Error(`${this.area} not supported in this environment`);
|
|
939
926
|
}
|
|
927
|
+
return this.getStore();
|
|
940
928
|
}
|
|
941
|
-
|
|
942
|
-
this.
|
|
929
|
+
getStore() {
|
|
930
|
+
return this.area === 'localStorage' ? window.localStorage : window.sessionStorage;
|
|
931
|
+
}
|
|
932
|
+
getKey(key, opts) {
|
|
933
|
+
const prefix = opts?.prefix ?? '';
|
|
934
|
+
return prefix ? `${prefix}:${key}` : key;
|
|
935
|
+
}
|
|
936
|
+
serializeValue(value, opts) {
|
|
937
|
+
if (opts?.serialize)
|
|
938
|
+
return opts.serialize(value);
|
|
939
|
+
return JSON.stringify(value);
|
|
940
|
+
}
|
|
941
|
+
deserializeValue(value, opts) {
|
|
942
|
+
if (opts?.deserialize)
|
|
943
|
+
return opts.deserialize(value);
|
|
943
944
|
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;
|
|
945
|
+
return JSON.parse(value);
|
|
950
946
|
}
|
|
951
|
-
catch
|
|
952
|
-
|
|
953
|
-
return false;
|
|
947
|
+
catch {
|
|
948
|
+
return value;
|
|
954
949
|
}
|
|
955
950
|
}
|
|
956
|
-
|
|
957
|
-
|
|
951
|
+
warnSecurityOnce() {
|
|
952
|
+
const holder = globalThis;
|
|
953
|
+
if (!holder[SECURITY_WARN_KEY])
|
|
954
|
+
holder[SECURITY_WARN_KEY] = new Set();
|
|
955
|
+
if (holder[SECURITY_WARN_KEY].has(this.area))
|
|
956
|
+
return;
|
|
957
|
+
holder[SECURITY_WARN_KEY].add(this.area);
|
|
958
|
+
this.logger.warn(`[storage:${this.area}] access denied (SecurityError). Common causes: Safari private mode, ` +
|
|
959
|
+
'sandboxed iframes, or browser storage disabled. Falling back to default values.');
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
let legacyDeprecationLogged$1 = false;
|
|
964
|
+
/**
|
|
965
|
+
* Web Storage service with two namespaces (`local`, `session`) sharing one method
|
|
966
|
+
* surface. SecurityError-safe (Safari private mode, sandboxed iframes return defaults
|
|
967
|
+
* instead of throwing).
|
|
968
|
+
*
|
|
969
|
+
* Preferred usage:
|
|
970
|
+
* ```ts
|
|
971
|
+
* const storage = inject(WebStorageService);
|
|
972
|
+
* storage.local.set('user', { id: 1 });
|
|
973
|
+
* const user = storage.local.get<{ id: number }>('user');
|
|
974
|
+
* storage.local.watch<{ id: number }>('user').subscribe(console.log);
|
|
975
|
+
* ```
|
|
976
|
+
*
|
|
977
|
+
* Legacy methods (`setLocalStorage`, `getLocalStorage`, etc.) remain as deprecated
|
|
978
|
+
* wrappers for one minor cycle; removal slated for v22.
|
|
979
|
+
*/
|
|
980
|
+
class WebStorageService extends BrowserApiBaseService {
|
|
981
|
+
storageLogger = inject(BROWSER_API_LOGGER);
|
|
982
|
+
storageEvents = signal(null, ...(ngDevMode ? [{ debugName: "storageEvents" }] : /* istanbul ignore next */ []));
|
|
983
|
+
eventBus = {
|
|
984
|
+
emit: (event) => this.storageEvents.set(event),
|
|
985
|
+
events$: toObservable(this.storageEvents).pipe(filter((event) => event !== null), distinctUntilChanged((a, b) => a.key === b.key &&
|
|
986
|
+
a.newValue === b.newValue &&
|
|
987
|
+
a.oldValue === b.oldValue &&
|
|
988
|
+
a.storageArea === b.storageArea)),
|
|
989
|
+
};
|
|
990
|
+
/** Local storage namespace. */
|
|
991
|
+
local = new StorageNamespaceImpl('localStorage', this.eventBus, this.storageLogger);
|
|
992
|
+
/** Session storage namespace. */
|
|
993
|
+
session = new StorageNamespaceImpl('sessionStorage', this.eventBus, this.storageLogger);
|
|
994
|
+
constructor() {
|
|
995
|
+
super();
|
|
996
|
+
this.setupCrossTabListener();
|
|
997
|
+
}
|
|
998
|
+
getApiName() {
|
|
999
|
+
return 'storage';
|
|
1000
|
+
}
|
|
1001
|
+
ensureSupported() {
|
|
1002
|
+
super.ensureSupported();
|
|
1003
|
+
if (typeof Storage === 'undefined') {
|
|
1004
|
+
throw new Error('Storage API not supported in this browser');
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/** Returns true if either local or session storage is usable. */
|
|
1008
|
+
isSupported() {
|
|
1009
|
+
return this.local.isSupported() || this.session.isSupported();
|
|
1010
|
+
}
|
|
1011
|
+
/** Stream of every storage mutation observed in this tab or other tabs. */
|
|
1012
|
+
getStorageEvents() {
|
|
1013
|
+
return this.eventBus.events$;
|
|
1014
|
+
}
|
|
1015
|
+
setupCrossTabListener() {
|
|
1016
|
+
if (!this.isBrowserEnvironment())
|
|
1017
|
+
return;
|
|
1018
|
+
fromEvent(window, 'storage')
|
|
1019
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
1020
|
+
.subscribe((event) => {
|
|
1021
|
+
const area = event.storageArea === window.localStorage ? 'localStorage' : 'sessionStorage';
|
|
1022
|
+
this.storageEvents.set({
|
|
1023
|
+
key: event.key,
|
|
1024
|
+
newValue: event.newValue ? this.safeParse(event.newValue) : null,
|
|
1025
|
+
oldValue: event.oldValue ? this.safeParse(event.oldValue) : null,
|
|
1026
|
+
storageArea: area,
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
safeParse(value) {
|
|
958
1031
|
try {
|
|
959
|
-
|
|
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;
|
|
1032
|
+
return JSON.parse(value);
|
|
979
1033
|
}
|
|
980
|
-
catch
|
|
981
|
-
|
|
982
|
-
return false;
|
|
1034
|
+
catch {
|
|
1035
|
+
return value;
|
|
983
1036
|
}
|
|
984
1037
|
}
|
|
985
|
-
//
|
|
1038
|
+
// ---------- legacy API (deprecated) ----------
|
|
1039
|
+
/** @deprecated Use `storage.local.set(key, value, opts)`. Removed in v22. */
|
|
1040
|
+
setLocalStorage(key, value, options = {}) {
|
|
1041
|
+
this.warnLegacyOnce();
|
|
1042
|
+
return this.local.set(key, value, options);
|
|
1043
|
+
}
|
|
1044
|
+
/** @deprecated Use `storage.local.get(key, defaultValue, opts)`. Removed in v22. */
|
|
1045
|
+
getLocalStorage(key, defaultValue = null, options = {}) {
|
|
1046
|
+
this.warnLegacyOnce();
|
|
1047
|
+
return this.local.get(key, defaultValue, options);
|
|
1048
|
+
}
|
|
1049
|
+
/** @deprecated Use `storage.local.remove(key, opts)`. Removed in v22. */
|
|
1050
|
+
removeLocalStorage(key, options = {}) {
|
|
1051
|
+
this.warnLegacyOnce();
|
|
1052
|
+
return this.local.remove(key, options);
|
|
1053
|
+
}
|
|
1054
|
+
/** @deprecated Use `storage.local.clear(opts)`. Removed in v22. */
|
|
1055
|
+
clearLocalStorage(options = {}) {
|
|
1056
|
+
this.warnLegacyOnce();
|
|
1057
|
+
return this.local.clear(options);
|
|
1058
|
+
}
|
|
1059
|
+
/** @deprecated Use `storage.session.set(key, value, opts)`. Removed in v22. */
|
|
1060
|
+
setSessionStorage(key, value, options = {}) {
|
|
1061
|
+
this.warnLegacyOnce();
|
|
1062
|
+
return this.session.set(key, value, options);
|
|
1063
|
+
}
|
|
1064
|
+
/** @deprecated Use `storage.session.get(key, defaultValue, opts)`. Removed in v22. */
|
|
1065
|
+
getSessionStorage(key, defaultValue = null, options = {}) {
|
|
1066
|
+
this.warnLegacyOnce();
|
|
1067
|
+
return this.session.get(key, defaultValue, options);
|
|
1068
|
+
}
|
|
1069
|
+
/** @deprecated Use `storage.session.remove(key, opts)`. Removed in v22. */
|
|
1070
|
+
removeSessionStorage(key, options = {}) {
|
|
1071
|
+
this.warnLegacyOnce();
|
|
1072
|
+
return this.session.remove(key, options);
|
|
1073
|
+
}
|
|
1074
|
+
/** @deprecated Use `storage.session.clear(opts)`. Removed in v22. */
|
|
1075
|
+
clearSessionStorage(options = {}) {
|
|
1076
|
+
this.warnLegacyOnce();
|
|
1077
|
+
return this.session.clear(options);
|
|
1078
|
+
}
|
|
1079
|
+
/** @deprecated Use `storage.local.size(opts)`. Removed in v22. */
|
|
986
1080
|
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;
|
|
1081
|
+
this.warnLegacyOnce();
|
|
1082
|
+
return this.local.size(options);
|
|
997
1083
|
}
|
|
1084
|
+
/** @deprecated Use `storage.session.size(opts)`. Removed in v22. */
|
|
998
1085
|
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));
|
|
1086
|
+
this.warnLegacyOnce();
|
|
1087
|
+
return this.session.size(options);
|
|
1014
1088
|
}
|
|
1089
|
+
/** @deprecated Use `storage.local.watch<T>(key, opts)`. Removed in v22. */
|
|
1015
1090
|
watchLocalStorage(key, options = {}) {
|
|
1016
|
-
|
|
1017
|
-
return this.
|
|
1091
|
+
this.warnLegacyOnce();
|
|
1092
|
+
return this.local.watch(key, options);
|
|
1018
1093
|
}
|
|
1094
|
+
/** @deprecated Use `storage.session.watch<T>(key, opts)`. Removed in v22. */
|
|
1019
1095
|
watchSessionStorage(key, options = {}) {
|
|
1020
|
-
|
|
1021
|
-
return this.
|
|
1096
|
+
this.warnLegacyOnce();
|
|
1097
|
+
return this.session.watch(key, options);
|
|
1022
1098
|
}
|
|
1023
|
-
|
|
1099
|
+
/** @deprecated Use `storage.local.native()`. Removed in v22. */
|
|
1024
1100
|
getNativeLocalStorage() {
|
|
1025
|
-
this.
|
|
1026
|
-
return
|
|
1101
|
+
this.warnLegacyOnce();
|
|
1102
|
+
return this.local.native();
|
|
1027
1103
|
}
|
|
1104
|
+
/** @deprecated Use `storage.session.native()`. Removed in v22. */
|
|
1028
1105
|
getNativeSessionStorage() {
|
|
1029
|
-
this.
|
|
1030
|
-
return
|
|
1106
|
+
this.warnLegacyOnce();
|
|
1107
|
+
return this.session.native();
|
|
1108
|
+
}
|
|
1109
|
+
warnLegacyOnce() {
|
|
1110
|
+
if (legacyDeprecationLogged$1)
|
|
1111
|
+
return;
|
|
1112
|
+
legacyDeprecationLogged$1 = true;
|
|
1113
|
+
this.storageLogger.warn('[storage] WebStorageService.{set,get,remove,clear,watch}{Local,Session}Storage are ' +
|
|
1114
|
+
'deprecated. Use storage.local and storage.session namespaces. Legacy methods will be ' +
|
|
1115
|
+
'removed in v22.');
|
|
1031
1116
|
}
|
|
1032
1117
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1033
1118
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebStorageService });
|
|
@@ -1036,19 +1121,288 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
1036
1121
|
type: Injectable
|
|
1037
1122
|
}], ctorParameters: () => [] });
|
|
1038
1123
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1124
|
+
const DEFAULT_MAX_RECONNECT_DELAY = 30_000;
|
|
1125
|
+
const DEFAULT_REQUEST_TIMEOUT = 30_000;
|
|
1126
|
+
/**
|
|
1127
|
+
* Stateful WebSocket client wrapping a single connection. One instance per logical
|
|
1128
|
+
* connection (do NOT share between `connect()` calls).
|
|
1129
|
+
*
|
|
1130
|
+
* Surfaces:
|
|
1131
|
+
* - `status`: signal of the current connection state.
|
|
1132
|
+
* - `messages$`: stream of every received message (parsed JSON).
|
|
1133
|
+
* - `send` / `sendRaw`: outbound traffic.
|
|
1134
|
+
* - `request<T>(type, data)`: round-trip with id correlation and timeout.
|
|
1135
|
+
* - `close`: idempotent disposal.
|
|
1136
|
+
*
|
|
1137
|
+
* Reconnect uses exponential backoff with jitter, capped by `maxReconnectDelay`.
|
|
1138
|
+
*/
|
|
1139
|
+
class WebSocketClient {
|
|
1140
|
+
config;
|
|
1141
|
+
logger;
|
|
1142
|
+
socket = null;
|
|
1049
1143
|
reconnectTimer = null;
|
|
1050
1144
|
heartbeatTimer = null;
|
|
1051
|
-
|
|
1145
|
+
_status;
|
|
1146
|
+
_messages$ = new Subject();
|
|
1147
|
+
pendingRequests = new Map();
|
|
1148
|
+
disposed = false;
|
|
1149
|
+
reconnectAttempts = 0;
|
|
1150
|
+
constructor(config, logger, destroyRef) {
|
|
1151
|
+
this.config = config;
|
|
1152
|
+
this.logger = logger;
|
|
1153
|
+
this._status = signal({
|
|
1154
|
+
state: 'idle',
|
|
1155
|
+
reconnectAttempts: 0,
|
|
1156
|
+
error: null,
|
|
1157
|
+
}, ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
|
|
1158
|
+
if (destroyRef) {
|
|
1159
|
+
destroyRef.onDestroy(() => this.close());
|
|
1160
|
+
}
|
|
1161
|
+
this.openSocket();
|
|
1162
|
+
}
|
|
1163
|
+
get status() {
|
|
1164
|
+
return this._status.asReadonly();
|
|
1165
|
+
}
|
|
1166
|
+
get messages$() {
|
|
1167
|
+
return this._messages$.asObservable();
|
|
1168
|
+
}
|
|
1169
|
+
messagesByType(type) {
|
|
1170
|
+
return this._messages$
|
|
1171
|
+
.asObservable()
|
|
1172
|
+
.pipe(filter((msg) => msg.type === type));
|
|
1173
|
+
}
|
|
1174
|
+
send(message) {
|
|
1175
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
1176
|
+
throw new Error('WebSocket is not connected');
|
|
1177
|
+
}
|
|
1178
|
+
const enriched = {
|
|
1179
|
+
...message,
|
|
1180
|
+
id: message.id ?? this.generateId(),
|
|
1181
|
+
timestamp: message.timestamp ?? Date.now(),
|
|
1182
|
+
};
|
|
1183
|
+
this.socket.send(JSON.stringify(enriched));
|
|
1184
|
+
}
|
|
1185
|
+
sendRaw(data) {
|
|
1186
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
1187
|
+
throw new Error('WebSocket is not connected');
|
|
1188
|
+
}
|
|
1189
|
+
this.socket.send(data);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Send a message and await a correlated response. The server MUST echo back the
|
|
1193
|
+
* `correlationId` from the request as `correlationId` on the response message.
|
|
1194
|
+
*/
|
|
1195
|
+
request(type, data, opts) {
|
|
1196
|
+
const id = this.generateId();
|
|
1197
|
+
const timeoutMs = opts?.timeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
1198
|
+
return new Promise((resolve, reject) => {
|
|
1199
|
+
const timer = setTimeout(() => {
|
|
1200
|
+
this.pendingRequests.delete(id);
|
|
1201
|
+
reject(new Error(`WebSocket request timeout after ${timeoutMs}ms`));
|
|
1202
|
+
}, timeoutMs);
|
|
1203
|
+
this.pendingRequests.set(id, {
|
|
1204
|
+
resolve: resolve,
|
|
1205
|
+
reject,
|
|
1206
|
+
timer,
|
|
1207
|
+
});
|
|
1208
|
+
try {
|
|
1209
|
+
this.send({ id, type, data, correlationId: id });
|
|
1210
|
+
}
|
|
1211
|
+
catch (error) {
|
|
1212
|
+
clearTimeout(timer);
|
|
1213
|
+
this.pendingRequests.delete(id);
|
|
1214
|
+
reject(error instanceof Error ? error : new Error('WebSocket send failed'));
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
close() {
|
|
1219
|
+
if (this.disposed)
|
|
1220
|
+
return;
|
|
1221
|
+
this.disposed = true;
|
|
1222
|
+
this.clearTimers();
|
|
1223
|
+
if (this.socket) {
|
|
1224
|
+
try {
|
|
1225
|
+
this.socket.close();
|
|
1226
|
+
}
|
|
1227
|
+
catch {
|
|
1228
|
+
// Ignore — already closing.
|
|
1229
|
+
}
|
|
1230
|
+
this.socket = null;
|
|
1231
|
+
}
|
|
1232
|
+
this.rejectAllPending(new Error('WebSocket closed'));
|
|
1233
|
+
this.updateStatus({ state: 'closed', error: null });
|
|
1234
|
+
}
|
|
1235
|
+
/** Internal handle for tests and advanced usage. */
|
|
1236
|
+
getNativeSocket() {
|
|
1237
|
+
return this.socket;
|
|
1238
|
+
}
|
|
1239
|
+
// ---------- internals ----------
|
|
1240
|
+
openSocket() {
|
|
1241
|
+
if (this.disposed)
|
|
1242
|
+
return;
|
|
1243
|
+
this.updateStatus({ state: 'connecting', error: null });
|
|
1244
|
+
try {
|
|
1245
|
+
this.socket = new WebSocket(this.config.url, this.config.protocols);
|
|
1246
|
+
this.attachHandlers();
|
|
1247
|
+
}
|
|
1248
|
+
catch (error) {
|
|
1249
|
+
const message = error instanceof Error ? error.message : 'WebSocket open failed';
|
|
1250
|
+
this.logger.error('[websocket] Failed to construct socket', error);
|
|
1251
|
+
this.updateStatus({ state: 'closed', error: message });
|
|
1252
|
+
this.scheduleReconnect();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
attachHandlers() {
|
|
1256
|
+
if (!this.socket)
|
|
1257
|
+
return;
|
|
1258
|
+
this.socket.onopen = () => {
|
|
1259
|
+
this.reconnectAttempts = 0;
|
|
1260
|
+
this.updateStatus({ state: 'open', error: null, reconnectAttempts: 0 });
|
|
1261
|
+
this.startHeartbeat();
|
|
1262
|
+
};
|
|
1263
|
+
this.socket.onclose = (event) => {
|
|
1264
|
+
this.stopHeartbeat();
|
|
1265
|
+
if (this.disposed)
|
|
1266
|
+
return;
|
|
1267
|
+
this.updateStatus({
|
|
1268
|
+
state: 'closed',
|
|
1269
|
+
error: event.wasClean ? null : `closed: ${event.code} ${event.reason}`,
|
|
1270
|
+
});
|
|
1271
|
+
if (!event.wasClean) {
|
|
1272
|
+
this.scheduleReconnect();
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
this.socket.onerror = () => {
|
|
1276
|
+
this.updateStatus({ error: 'WebSocket connection error' });
|
|
1277
|
+
};
|
|
1278
|
+
this.socket.onmessage = (event) => {
|
|
1279
|
+
this.handleIncoming(event.data);
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
handleIncoming(raw) {
|
|
1283
|
+
let message;
|
|
1284
|
+
try {
|
|
1285
|
+
const text = typeof raw === 'string' ? raw : String(raw);
|
|
1286
|
+
message = JSON.parse(text);
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
this.logger.warn('[websocket] Failed to parse incoming message');
|
|
1290
|
+
this.logger.error('[websocket] parse error', error);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const correlationId = message.correlationId ?? message.id;
|
|
1294
|
+
if (correlationId && this.pendingRequests.has(correlationId)) {
|
|
1295
|
+
const pending = this.pendingRequests.get(correlationId);
|
|
1296
|
+
clearTimeout(pending.timer);
|
|
1297
|
+
this.pendingRequests.delete(correlationId);
|
|
1298
|
+
pending.resolve(message.data);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
this._messages$.next(message);
|
|
1302
|
+
}
|
|
1303
|
+
scheduleReconnect() {
|
|
1304
|
+
if (this.disposed)
|
|
1305
|
+
return;
|
|
1306
|
+
const interval = this.config.reconnectInterval ?? 0;
|
|
1307
|
+
const maxAttempts = this.config.maxReconnectAttempts ?? 0;
|
|
1308
|
+
if (interval <= 0 || maxAttempts <= 0)
|
|
1309
|
+
return;
|
|
1310
|
+
if (this.reconnectAttempts >= maxAttempts) {
|
|
1311
|
+
this.updateStatus({
|
|
1312
|
+
state: 'closed',
|
|
1313
|
+
error: `Max reconnect attempts (${maxAttempts}) reached`,
|
|
1314
|
+
});
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
this.reconnectAttempts += 1;
|
|
1318
|
+
const delay = WebSocketClient.computeBackoffDelay(this.reconnectAttempts, interval, this.config.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_DELAY);
|
|
1319
|
+
this.updateStatus({
|
|
1320
|
+
state: 'reconnecting',
|
|
1321
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
1322
|
+
});
|
|
1323
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1324
|
+
this.reconnectTimer = null;
|
|
1325
|
+
this.openSocket();
|
|
1326
|
+
}, delay);
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Exponential backoff with full jitter:
|
|
1330
|
+
* baseDelay = min(maxDelay, interval * 2^(attempt - 1))
|
|
1331
|
+
* delay = random(0, baseDelay)
|
|
1332
|
+
*/
|
|
1333
|
+
static computeBackoffDelay(attempt, interval, maxDelay) {
|
|
1334
|
+
const exp = Math.min(maxDelay, interval * Math.pow(2, attempt - 1));
|
|
1335
|
+
return Math.floor(Math.random() * exp);
|
|
1336
|
+
}
|
|
1337
|
+
startHeartbeat() {
|
|
1338
|
+
const { heartbeatInterval, heartbeatMessage } = this.config;
|
|
1339
|
+
if (!heartbeatInterval || heartbeatMessage === undefined)
|
|
1340
|
+
return;
|
|
1341
|
+
this.stopHeartbeat();
|
|
1342
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1343
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
1344
|
+
try {
|
|
1345
|
+
this.send({ type: 'heartbeat', data: heartbeatMessage });
|
|
1346
|
+
}
|
|
1347
|
+
catch (error) {
|
|
1348
|
+
this.logger.warn('[websocket] heartbeat send failed');
|
|
1349
|
+
this.logger.error('[websocket] heartbeat error', error);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}, heartbeatInterval);
|
|
1353
|
+
}
|
|
1354
|
+
stopHeartbeat() {
|
|
1355
|
+
if (this.heartbeatTimer) {
|
|
1356
|
+
clearInterval(this.heartbeatTimer);
|
|
1357
|
+
this.heartbeatTimer = null;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
clearTimers() {
|
|
1361
|
+
if (this.reconnectTimer) {
|
|
1362
|
+
clearTimeout(this.reconnectTimer);
|
|
1363
|
+
this.reconnectTimer = null;
|
|
1364
|
+
}
|
|
1365
|
+
this.stopHeartbeat();
|
|
1366
|
+
}
|
|
1367
|
+
rejectAllPending(reason) {
|
|
1368
|
+
this.pendingRequests.forEach((entry) => {
|
|
1369
|
+
clearTimeout(entry.timer);
|
|
1370
|
+
entry.reject(reason);
|
|
1371
|
+
});
|
|
1372
|
+
this.pendingRequests.clear();
|
|
1373
|
+
}
|
|
1374
|
+
updateStatus(partial) {
|
|
1375
|
+
this._status.update((current) => ({ ...current, ...partial }));
|
|
1376
|
+
}
|
|
1377
|
+
generateId() {
|
|
1378
|
+
if (typeof globalThis.crypto?.randomUUID === 'function') {
|
|
1379
|
+
return globalThis.crypto.randomUUID();
|
|
1380
|
+
}
|
|
1381
|
+
return `ws-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
let legacyDeprecationLogged = false;
|
|
1386
|
+
/**
|
|
1387
|
+
* Service that creates and tracks `WebSocketClient` instances.
|
|
1388
|
+
*
|
|
1389
|
+
* Preferred usage:
|
|
1390
|
+
* ```ts
|
|
1391
|
+
* const ws = inject(WebSocketService);
|
|
1392
|
+
* const client = ws.createClient({ url: 'wss://...' });
|
|
1393
|
+
* effect(() => console.log(client.status()));
|
|
1394
|
+
* await client.request('ping', {});
|
|
1395
|
+
* ```
|
|
1396
|
+
*
|
|
1397
|
+
* Legacy usage (`connect()` returning Observable) is preserved for one minor cycle
|
|
1398
|
+
* and will be removed in v22.
|
|
1399
|
+
*/
|
|
1400
|
+
class WebSocketService extends BrowserApiBaseService {
|
|
1401
|
+
wsLogger = inject(BROWSER_API_LOGGER);
|
|
1402
|
+
clients = new Set();
|
|
1403
|
+
_cleanup = this.destroyRef.onDestroy(() => this.disposeAll());
|
|
1404
|
+
/** Legacy single-connection holder used by deprecated `connect()`/`send()` API. */
|
|
1405
|
+
legacyClient = null;
|
|
1052
1406
|
getApiName() {
|
|
1053
1407
|
return 'websocket';
|
|
1054
1408
|
}
|
|
@@ -1058,174 +1412,133 @@ class WebSocketService extends BrowserApiBaseService {
|
|
|
1058
1412
|
throw new Error('WebSocket API not supported in this browser');
|
|
1059
1413
|
}
|
|
1060
1414
|
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Create a new WebSocket client. The client owns one connection and is the recommended
|
|
1417
|
+
* surface for all interactions (status signal, request/response, reconnect, etc.).
|
|
1418
|
+
*
|
|
1419
|
+
* The returned client is automatically disposed when the host injector is destroyed.
|
|
1420
|
+
*/
|
|
1421
|
+
createClient(config) {
|
|
1422
|
+
this.ensureSupported();
|
|
1423
|
+
const client = new WebSocketClient(config, this.wsLogger, this.destroyRef);
|
|
1424
|
+
this.clients.add(client);
|
|
1425
|
+
return client;
|
|
1426
|
+
}
|
|
1427
|
+
/** Dispose every client created via `createClient()` (also called automatically on destroy). */
|
|
1428
|
+
disposeAll() {
|
|
1429
|
+
for (const client of this.clients) {
|
|
1430
|
+
client.close();
|
|
1431
|
+
}
|
|
1432
|
+
this.clients.clear();
|
|
1433
|
+
if (this.legacyClient) {
|
|
1434
|
+
this.legacyClient.close();
|
|
1435
|
+
this.legacyClient = null;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
// ---------- legacy API (deprecated) ----------
|
|
1439
|
+
/**
|
|
1440
|
+
* @deprecated Use {@link createClient} which returns a `WebSocketClient` exposing a
|
|
1441
|
+
* status signal, request/response, and proper reconnect. This wrapper will be removed
|
|
1442
|
+
* in v22.
|
|
1443
|
+
*/
|
|
1061
1444
|
connect(config) {
|
|
1062
1445
|
this.ensureSupported();
|
|
1446
|
+
this.warnLegacyOnce();
|
|
1063
1447
|
return new Observable((observer) => {
|
|
1064
|
-
this.
|
|
1065
|
-
|
|
1066
|
-
connected: false,
|
|
1067
|
-
connecting: true,
|
|
1068
|
-
reconnecting: false,
|
|
1069
|
-
reconnectAttempts: 0,
|
|
1070
|
-
});
|
|
1071
|
-
try {
|
|
1072
|
-
this.webSocket = new WebSocket(config.url, config.protocols);
|
|
1073
|
-
this.setupWebSocketHandlers(config);
|
|
1074
|
-
observer.next(this.statusSubject.getValue());
|
|
1075
|
-
}
|
|
1076
|
-
catch (error) {
|
|
1077
|
-
this.logError('Error creating WebSocket:', error);
|
|
1078
|
-
this.updateStatus({
|
|
1079
|
-
connected: false,
|
|
1080
|
-
connecting: false,
|
|
1081
|
-
reconnecting: false,
|
|
1082
|
-
error: error instanceof Error ? error.message : 'Connection failed',
|
|
1083
|
-
reconnectAttempts: 0,
|
|
1084
|
-
});
|
|
1085
|
-
observer.next(this.statusSubject.getValue());
|
|
1448
|
+
if (this.legacyClient) {
|
|
1449
|
+
this.legacyClient.close();
|
|
1086
1450
|
}
|
|
1451
|
+
const client = new WebSocketClient(config, this.wsLogger);
|
|
1452
|
+
this.legacyClient = client;
|
|
1453
|
+
const sub = toObservableLike(client).subscribe({
|
|
1454
|
+
next: (status) => observer.next(status),
|
|
1455
|
+
error: (err) => observer.error(err),
|
|
1456
|
+
});
|
|
1087
1457
|
return () => {
|
|
1088
|
-
|
|
1458
|
+
sub.unsubscribe();
|
|
1459
|
+
client.close();
|
|
1460
|
+
if (this.legacyClient === client) {
|
|
1461
|
+
this.legacyClient = null;
|
|
1462
|
+
}
|
|
1089
1463
|
};
|
|
1090
1464
|
});
|
|
1091
1465
|
}
|
|
1466
|
+
/** @deprecated Use {@link createClient} and call `client.close()`. */
|
|
1092
1467
|
disconnect() {
|
|
1093
|
-
if (this.
|
|
1094
|
-
|
|
1095
|
-
this.
|
|
1096
|
-
}
|
|
1097
|
-
if (this.heartbeatTimer) {
|
|
1098
|
-
clearInterval(this.heartbeatTimer);
|
|
1099
|
-
this.heartbeatTimer = null;
|
|
1100
|
-
}
|
|
1101
|
-
if (this.webSocket) {
|
|
1102
|
-
this.webSocket.close();
|
|
1103
|
-
this.webSocket = null;
|
|
1468
|
+
if (this.legacyClient) {
|
|
1469
|
+
this.legacyClient.close();
|
|
1470
|
+
this.legacyClient = null;
|
|
1104
1471
|
}
|
|
1105
|
-
this.updateStatus({
|
|
1106
|
-
connected: false,
|
|
1107
|
-
connecting: false,
|
|
1108
|
-
reconnecting: false,
|
|
1109
|
-
reconnectAttempts: 0,
|
|
1110
|
-
});
|
|
1111
1472
|
}
|
|
1473
|
+
/** @deprecated Use the client returned by {@link createClient}. */
|
|
1112
1474
|
send(message) {
|
|
1113
|
-
if (!this.
|
|
1114
|
-
throw new Error('WebSocket
|
|
1475
|
+
if (!this.legacyClient) {
|
|
1476
|
+
throw new Error('No active legacy WebSocket. Call connect() first or use createClient().');
|
|
1115
1477
|
}
|
|
1116
|
-
|
|
1117
|
-
...message,
|
|
1118
|
-
timestamp: Date.now(),
|
|
1119
|
-
};
|
|
1120
|
-
this.webSocket.send(JSON.stringify(messageWithTimestamp));
|
|
1478
|
+
this.legacyClient.send(message);
|
|
1121
1479
|
}
|
|
1480
|
+
/** @deprecated Use the client returned by {@link createClient}. */
|
|
1122
1481
|
sendRaw(data) {
|
|
1123
|
-
if (!this.
|
|
1124
|
-
throw new Error('WebSocket
|
|
1482
|
+
if (!this.legacyClient) {
|
|
1483
|
+
throw new Error('No active legacy WebSocket. Call connect() first or use createClient().');
|
|
1125
1484
|
}
|
|
1126
|
-
this.
|
|
1485
|
+
this.legacyClient.sendRaw(data);
|
|
1127
1486
|
}
|
|
1487
|
+
/** @deprecated Use `client.status` from {@link createClient}. */
|
|
1128
1488
|
getStatus() {
|
|
1129
|
-
return
|
|
1489
|
+
return new Observable((observer) => {
|
|
1490
|
+
if (!this.legacyClient) {
|
|
1491
|
+
observer.next({
|
|
1492
|
+
connected: false,
|
|
1493
|
+
connecting: false,
|
|
1494
|
+
reconnecting: false,
|
|
1495
|
+
reconnectAttempts: 0,
|
|
1496
|
+
});
|
|
1497
|
+
return () => {
|
|
1498
|
+
// No-op
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
const sub = toObservableLike(this.legacyClient).subscribe((status) => observer.next(status));
|
|
1502
|
+
return () => sub.unsubscribe();
|
|
1503
|
+
});
|
|
1130
1504
|
}
|
|
1505
|
+
/** @deprecated Use `client.messages$` from {@link createClient}. */
|
|
1131
1506
|
getMessages() {
|
|
1132
|
-
|
|
1507
|
+
if (!this.legacyClient) {
|
|
1508
|
+
return new Observable(() => {
|
|
1509
|
+
// No-op stream until connected.
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
return this.legacyClient.messages$;
|
|
1133
1513
|
}
|
|
1514
|
+
/** @deprecated Use `client.messagesByType()` from {@link createClient}. */
|
|
1134
1515
|
getMessagesByType(type) {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
setupWebSocketHandlers(config) {
|
|
1140
|
-
if (!this.webSocket)
|
|
1141
|
-
return;
|
|
1142
|
-
this.webSocket.onopen = () => {
|
|
1143
|
-
this.logInfo(`Connected to: ${config.url}`);
|
|
1144
|
-
this.reconnectAttempts = 0;
|
|
1145
|
-
this.updateStatus({
|
|
1146
|
-
connected: true,
|
|
1147
|
-
connecting: false,
|
|
1148
|
-
reconnecting: false,
|
|
1149
|
-
reconnectAttempts: 0,
|
|
1150
|
-
});
|
|
1151
|
-
// Start heartbeat if configured
|
|
1152
|
-
if (config.heartbeatInterval && config.heartbeatMessage) {
|
|
1153
|
-
this.startHeartbeat(config);
|
|
1154
|
-
}
|
|
1155
|
-
};
|
|
1156
|
-
this.webSocket.onclose = (event) => {
|
|
1157
|
-
this.logInfo(`Connection closed: ${event.code} ${event.reason}`);
|
|
1158
|
-
this.updateStatus({
|
|
1159
|
-
connected: false,
|
|
1160
|
-
connecting: false,
|
|
1161
|
-
reconnecting: false,
|
|
1162
|
-
reconnectAttempts: this.reconnectAttempts,
|
|
1163
|
-
});
|
|
1164
|
-
// Attempt reconnection if not a clean close and reconnect is enabled
|
|
1165
|
-
if (!event.wasClean && config.reconnectInterval && config.maxReconnectAttempts) {
|
|
1166
|
-
this.attemptReconnect(config);
|
|
1167
|
-
}
|
|
1168
|
-
};
|
|
1169
|
-
this.webSocket.onerror = (error) => {
|
|
1170
|
-
this.logError('WebSocket error:', error);
|
|
1171
|
-
this.updateStatus({
|
|
1172
|
-
connected: false,
|
|
1173
|
-
connecting: false,
|
|
1174
|
-
reconnecting: false,
|
|
1175
|
-
error: 'WebSocket connection error',
|
|
1176
|
-
reconnectAttempts: this.reconnectAttempts,
|
|
1516
|
+
if (!this.legacyClient) {
|
|
1517
|
+
return new Observable(() => {
|
|
1518
|
+
// No-op stream until connected.
|
|
1177
1519
|
});
|
|
1178
|
-
};
|
|
1179
|
-
this.webSocket.onmessage = (event) => {
|
|
1180
|
-
try {
|
|
1181
|
-
const message = JSON.parse(event.data);
|
|
1182
|
-
this.messageSubject.next(message);
|
|
1183
|
-
}
|
|
1184
|
-
catch (error) {
|
|
1185
|
-
this.logError('Error parsing message:', error);
|
|
1186
|
-
}
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
startHeartbeat(config) {
|
|
1190
|
-
this.heartbeatTimer = setInterval(() => {
|
|
1191
|
-
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
|
|
1192
|
-
this.send({
|
|
1193
|
-
type: 'heartbeat',
|
|
1194
|
-
data: config.heartbeatMessage,
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
}, config.heartbeatInterval);
|
|
1198
|
-
}
|
|
1199
|
-
attemptReconnect(config) {
|
|
1200
|
-
if (this.reconnectAttempts >= (config.maxReconnectAttempts || 5)) {
|
|
1201
|
-
this.logInfo('Max reconnect attempts reached');
|
|
1202
|
-
return;
|
|
1203
1520
|
}
|
|
1204
|
-
this.
|
|
1205
|
-
this.updateStatus({
|
|
1206
|
-
connected: false,
|
|
1207
|
-
connecting: false,
|
|
1208
|
-
reconnecting: true,
|
|
1209
|
-
reconnectAttempts: this.reconnectAttempts,
|
|
1210
|
-
});
|
|
1211
|
-
this.reconnectTimer = setTimeout(() => {
|
|
1212
|
-
this.logInfo(`Reconnect attempt ${this.reconnectAttempts}`);
|
|
1213
|
-
this.connect(config);
|
|
1214
|
-
}, config.reconnectInterval || 3000);
|
|
1215
|
-
}
|
|
1216
|
-
updateStatus(status) {
|
|
1217
|
-
const newStatus = { ...this.statusSubject.getValue(), ...status };
|
|
1218
|
-
this.statusSubject.next(newStatus);
|
|
1521
|
+
return this.legacyClient.messagesByType(type);
|
|
1219
1522
|
}
|
|
1220
|
-
|
|
1523
|
+
/** @deprecated Use the client returned by {@link createClient}. */
|
|
1221
1524
|
getNativeWebSocket() {
|
|
1222
|
-
return this.
|
|
1525
|
+
return this.legacyClient?.getNativeSocket() ?? null;
|
|
1223
1526
|
}
|
|
1527
|
+
/** @deprecated Use `client.status()` from {@link createClient}. */
|
|
1224
1528
|
isConnected() {
|
|
1225
|
-
return this.
|
|
1529
|
+
return this.legacyClient?.status().state === 'open';
|
|
1226
1530
|
}
|
|
1531
|
+
/** @deprecated Use the native socket via `client.getNativeSocket()`. */
|
|
1227
1532
|
getReadyState() {
|
|
1228
|
-
return this.
|
|
1533
|
+
return this.legacyClient?.getNativeSocket()?.readyState ?? WebSocket.CLOSED;
|
|
1534
|
+
}
|
|
1535
|
+
warnLegacyOnce() {
|
|
1536
|
+
if (legacyDeprecationLogged)
|
|
1537
|
+
return;
|
|
1538
|
+
legacyDeprecationLogged = true;
|
|
1539
|
+
this.wsLogger.warn('[websocket] WebSocketService.connect() is deprecated. Use WebSocketService.createClient() ' +
|
|
1540
|
+
'which returns a WebSocketClient with a status signal, request/response, and proper reconnect. ' +
|
|
1541
|
+
'The legacy API will be removed in v22.');
|
|
1229
1542
|
}
|
|
1230
1543
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
1231
1544
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService });
|
|
@@ -1233,6 +1546,27 @@ class WebSocketService extends BrowserApiBaseService {
|
|
|
1233
1546
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebSocketService, decorators: [{
|
|
1234
1547
|
type: Injectable
|
|
1235
1548
|
}] });
|
|
1549
|
+
/**
|
|
1550
|
+
* Build a stream of legacy `WebSocketStatus` snapshots from a v2 client. Used to keep the
|
|
1551
|
+
* deprecated `connect()` API behaving like before (Observable of legacy status).
|
|
1552
|
+
*/
|
|
1553
|
+
function toObservableLike(client) {
|
|
1554
|
+
return new Observable((observer) => {
|
|
1555
|
+
const emit = () => {
|
|
1556
|
+
const v2 = client.status();
|
|
1557
|
+
observer.next({
|
|
1558
|
+
connected: v2.state === 'open',
|
|
1559
|
+
connecting: v2.state === 'connecting',
|
|
1560
|
+
reconnecting: v2.state === 'reconnecting',
|
|
1561
|
+
error: v2.error ?? undefined,
|
|
1562
|
+
reconnectAttempts: v2.reconnectAttempts,
|
|
1563
|
+
});
|
|
1564
|
+
};
|
|
1565
|
+
emit();
|
|
1566
|
+
const id = setInterval(emit, 100);
|
|
1567
|
+
return () => clearInterval(id);
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1236
1570
|
|
|
1237
1571
|
class WebWorkerService extends BrowserApiBaseService {
|
|
1238
1572
|
workers = new Map();
|
|
@@ -2441,106 +2775,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
2441
2775
|
type: Injectable
|
|
2442
2776
|
}] });
|
|
2443
2777
|
|
|
2444
|
-
function getIdleDetectorClass$1() {
|
|
2445
|
-
return window.IdleDetector;
|
|
2446
|
-
}
|
|
2447
|
-
class IdleDetectorService extends BrowserApiBaseService {
|
|
2448
|
-
getApiName() {
|
|
2449
|
-
return 'idle-detector';
|
|
2450
|
-
}
|
|
2451
|
-
isSupported() {
|
|
2452
|
-
return this.isBrowserEnvironment() && 'IdleDetector' in window;
|
|
2453
|
-
}
|
|
2454
|
-
async requestPermission() {
|
|
2455
|
-
if (!this.isSupported()) {
|
|
2456
|
-
throw new Error('IdleDetector API not supported');
|
|
2457
|
-
}
|
|
2458
|
-
return getIdleDetectorClass$1().requestPermission();
|
|
2459
|
-
}
|
|
2460
|
-
watch(options = {}) {
|
|
2461
|
-
if (!this.isSupported()) {
|
|
2462
|
-
return new Observable((o) => o.error(new Error('IdleDetector API not supported')));
|
|
2463
|
-
}
|
|
2464
|
-
return new Observable((subscriber) => {
|
|
2465
|
-
const abortController = new AbortController();
|
|
2466
|
-
const detector = new (getIdleDetectorClass$1())();
|
|
2467
|
-
detector.addEventListener('change', () => {
|
|
2468
|
-
subscriber.next({
|
|
2469
|
-
user: detector.userState,
|
|
2470
|
-
screen: detector.screenState,
|
|
2471
|
-
});
|
|
2472
|
-
});
|
|
2473
|
-
detector
|
|
2474
|
-
.start({
|
|
2475
|
-
threshold: options.threshold ?? 60_000,
|
|
2476
|
-
signal: abortController.signal,
|
|
2477
|
-
})
|
|
2478
|
-
.catch((err) => subscriber.error(err));
|
|
2479
|
-
return () => abortController.abort();
|
|
2480
|
-
});
|
|
2481
|
-
}
|
|
2482
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IdleDetectorService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2483
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IdleDetectorService });
|
|
2484
|
-
}
|
|
2485
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: IdleDetectorService, decorators: [{
|
|
2486
|
-
type: Injectable
|
|
2487
|
-
}] });
|
|
2488
|
-
|
|
2489
|
-
function getEyeDropperClass() {
|
|
2490
|
-
return window.EyeDropper;
|
|
2491
|
-
}
|
|
2492
|
-
class EyeDropperService extends BrowserApiBaseService {
|
|
2493
|
-
getApiName() {
|
|
2494
|
-
return 'eye-dropper';
|
|
2495
|
-
}
|
|
2496
|
-
isSupported() {
|
|
2497
|
-
return this.isBrowserEnvironment() && 'EyeDropper' in window;
|
|
2498
|
-
}
|
|
2499
|
-
async open(signal) {
|
|
2500
|
-
if (!this.isSupported()) {
|
|
2501
|
-
throw new Error('EyeDropper API not supported');
|
|
2502
|
-
}
|
|
2503
|
-
const dropper = new (getEyeDropperClass())();
|
|
2504
|
-
return dropper.open(signal ? { signal } : undefined);
|
|
2505
|
-
}
|
|
2506
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: EyeDropperService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2507
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: EyeDropperService });
|
|
2508
|
-
}
|
|
2509
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: EyeDropperService, decorators: [{
|
|
2510
|
-
type: Injectable
|
|
2511
|
-
}] });
|
|
2512
|
-
|
|
2513
|
-
function getBarcodeDetectorClass() {
|
|
2514
|
-
return window.BarcodeDetector;
|
|
2515
|
-
}
|
|
2516
|
-
class BarcodeDetectorService extends BrowserApiBaseService {
|
|
2517
|
-
getApiName() {
|
|
2518
|
-
return 'barcode-detector';
|
|
2519
|
-
}
|
|
2520
|
-
isSupported() {
|
|
2521
|
-
return this.isBrowserEnvironment() && 'BarcodeDetector' in window;
|
|
2522
|
-
}
|
|
2523
|
-
async getSupportedFormats() {
|
|
2524
|
-
if (!this.isSupported())
|
|
2525
|
-
return [];
|
|
2526
|
-
return getBarcodeDetectorClass().getSupportedFormats();
|
|
2527
|
-
}
|
|
2528
|
-
async detect(image, formats) {
|
|
2529
|
-
if (!this.isSupported()) {
|
|
2530
|
-
throw new Error('BarcodeDetector API not supported');
|
|
2531
|
-
}
|
|
2532
|
-
const detector = formats
|
|
2533
|
-
? new (getBarcodeDetectorClass())({ formats })
|
|
2534
|
-
: new (getBarcodeDetectorClass())();
|
|
2535
|
-
return detector.detect(image);
|
|
2536
|
-
}
|
|
2537
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BarcodeDetectorService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2538
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BarcodeDetectorService });
|
|
2539
|
-
}
|
|
2540
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: BarcodeDetectorService, decorators: [{
|
|
2541
|
-
type: Injectable
|
|
2542
|
-
}] });
|
|
2543
|
-
|
|
2544
2778
|
class WebAudioService extends BrowserApiBaseService {
|
|
2545
2779
|
getApiName() {
|
|
2546
2780
|
return 'web-audio';
|
|
@@ -2750,301 +2984,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
2750
2984
|
type: Injectable
|
|
2751
2985
|
}] });
|
|
2752
2986
|
|
|
2753
|
-
function getBluetooth() {
|
|
2754
|
-
return navigator.bluetooth;
|
|
2755
|
-
}
|
|
2756
|
-
class WebBluetoothService extends BrowserApiBaseService {
|
|
2757
|
-
getApiName() {
|
|
2758
|
-
return 'web-bluetooth';
|
|
2759
|
-
}
|
|
2760
|
-
isSupported() {
|
|
2761
|
-
return this.isBrowserEnvironment() && !!getBluetooth();
|
|
2762
|
-
}
|
|
2763
|
-
async requestDevice(options = { acceptAllDevices: true }) {
|
|
2764
|
-
if (!this.isSupported()) {
|
|
2765
|
-
throw new Error('Web Bluetooth API not supported');
|
|
2766
|
-
}
|
|
2767
|
-
return getBluetooth().requestDevice(options);
|
|
2768
|
-
}
|
|
2769
|
-
async connect(device) {
|
|
2770
|
-
if (!device.gatt) {
|
|
2771
|
-
throw new Error('GATT server not available on this device');
|
|
2772
|
-
}
|
|
2773
|
-
return device.gatt.connect();
|
|
2774
|
-
}
|
|
2775
|
-
disconnect(device) {
|
|
2776
|
-
device.gatt?.disconnect();
|
|
2777
|
-
}
|
|
2778
|
-
watchDisconnection(device) {
|
|
2779
|
-
return new Observable((subscriber) => {
|
|
2780
|
-
const handler = () => subscriber.next();
|
|
2781
|
-
device.addEventListener('gattserverdisconnected', handler);
|
|
2782
|
-
return () => device.removeEventListener('gattserverdisconnected', handler);
|
|
2783
|
-
});
|
|
2784
|
-
}
|
|
2785
|
-
async readCharacteristic(server, serviceUuid, characteristicUuid) {
|
|
2786
|
-
const service = await server.getPrimaryService(serviceUuid);
|
|
2787
|
-
const characteristic = await service.getCharacteristic(characteristicUuid);
|
|
2788
|
-
return characteristic.readValue();
|
|
2789
|
-
}
|
|
2790
|
-
async writeCharacteristic(server, serviceUuid, characteristicUuid, value) {
|
|
2791
|
-
const service = await server.getPrimaryService(serviceUuid);
|
|
2792
|
-
const characteristic = await service.getCharacteristic(characteristicUuid);
|
|
2793
|
-
await characteristic.writeValue(value);
|
|
2794
|
-
}
|
|
2795
|
-
watchCharacteristic(server, serviceUuid, characteristicUuid) {
|
|
2796
|
-
return new Observable((subscriber) => {
|
|
2797
|
-
let characteristic;
|
|
2798
|
-
server
|
|
2799
|
-
.getPrimaryService(serviceUuid)
|
|
2800
|
-
.then((service) => service.getCharacteristic(characteristicUuid))
|
|
2801
|
-
.then((char) => {
|
|
2802
|
-
characteristic = char;
|
|
2803
|
-
const handler = (event) => {
|
|
2804
|
-
const target = event.target;
|
|
2805
|
-
if (target.value)
|
|
2806
|
-
subscriber.next(target.value);
|
|
2807
|
-
};
|
|
2808
|
-
characteristic.addEventListener('characteristicvaluechanged', handler);
|
|
2809
|
-
return characteristic.startNotifications();
|
|
2810
|
-
})
|
|
2811
|
-
.catch((err) => subscriber.error(err));
|
|
2812
|
-
return () => {
|
|
2813
|
-
characteristic?.stopNotifications().catch(() => { });
|
|
2814
|
-
};
|
|
2815
|
-
});
|
|
2816
|
-
}
|
|
2817
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebBluetoothService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2818
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebBluetoothService });
|
|
2819
|
-
}
|
|
2820
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebBluetoothService, decorators: [{
|
|
2821
|
-
type: Injectable
|
|
2822
|
-
}] });
|
|
2823
|
-
|
|
2824
|
-
function getUsb() {
|
|
2825
|
-
return navigator.usb;
|
|
2826
|
-
}
|
|
2827
|
-
class WebUsbService extends BrowserApiBaseService {
|
|
2828
|
-
getApiName() {
|
|
2829
|
-
return 'web-usb';
|
|
2830
|
-
}
|
|
2831
|
-
isSupported() {
|
|
2832
|
-
return this.isBrowserEnvironment() && !!getUsb();
|
|
2833
|
-
}
|
|
2834
|
-
async requestDevice(filters = []) {
|
|
2835
|
-
if (!this.isSupported()) {
|
|
2836
|
-
throw new Error('WebUSB API not supported');
|
|
2837
|
-
}
|
|
2838
|
-
return getUsb().requestDevice({ filters });
|
|
2839
|
-
}
|
|
2840
|
-
async getDevices() {
|
|
2841
|
-
if (!this.isSupported())
|
|
2842
|
-
return [];
|
|
2843
|
-
return getUsb().getDevices();
|
|
2844
|
-
}
|
|
2845
|
-
async open(device) {
|
|
2846
|
-
await device.open();
|
|
2847
|
-
}
|
|
2848
|
-
async close(device) {
|
|
2849
|
-
await device.close();
|
|
2850
|
-
}
|
|
2851
|
-
async selectConfiguration(device, configurationValue) {
|
|
2852
|
-
await device.selectConfiguration(configurationValue);
|
|
2853
|
-
}
|
|
2854
|
-
async claimInterface(device, interfaceNumber) {
|
|
2855
|
-
await device.claimInterface(interfaceNumber);
|
|
2856
|
-
}
|
|
2857
|
-
async releaseInterface(device, interfaceNumber) {
|
|
2858
|
-
await device.releaseInterface(interfaceNumber);
|
|
2859
|
-
}
|
|
2860
|
-
async transferIn(device, endpointNumber, length) {
|
|
2861
|
-
return device.transferIn(endpointNumber, length);
|
|
2862
|
-
}
|
|
2863
|
-
async transferOut(device, endpointNumber, data) {
|
|
2864
|
-
return device.transferOut(endpointNumber, data);
|
|
2865
|
-
}
|
|
2866
|
-
watchConnection() {
|
|
2867
|
-
if (!this.isSupported()) {
|
|
2868
|
-
return new Observable((o) => o.error(new Error('WebUSB API not supported')));
|
|
2869
|
-
}
|
|
2870
|
-
return new Observable((subscriber) => {
|
|
2871
|
-
const usb = getUsb();
|
|
2872
|
-
const onConnect = (e) => subscriber.next({ device: e.device, type: 'connect' });
|
|
2873
|
-
const onDisconnect = (e) => subscriber.next({ device: e.device, type: 'disconnect' });
|
|
2874
|
-
usb.addEventListener('connect', onConnect);
|
|
2875
|
-
usb.addEventListener('disconnect', onDisconnect);
|
|
2876
|
-
return () => {
|
|
2877
|
-
usb.removeEventListener('connect', onConnect);
|
|
2878
|
-
usb.removeEventListener('disconnect', onDisconnect);
|
|
2879
|
-
};
|
|
2880
|
-
});
|
|
2881
|
-
}
|
|
2882
|
-
getDeviceInfo(device) {
|
|
2883
|
-
return {
|
|
2884
|
-
vendorId: device.vendorId,
|
|
2885
|
-
productId: device.productId,
|
|
2886
|
-
productName: device.productName,
|
|
2887
|
-
manufacturerName: device.manufacturerName,
|
|
2888
|
-
serialNumber: device.serialNumber,
|
|
2889
|
-
opened: device.opened,
|
|
2890
|
-
};
|
|
2891
|
-
}
|
|
2892
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebUsbService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2893
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebUsbService });
|
|
2894
|
-
}
|
|
2895
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebUsbService, decorators: [{
|
|
2896
|
-
type: Injectable
|
|
2897
|
-
}] });
|
|
2898
|
-
|
|
2899
|
-
function getNdefReaderClass() {
|
|
2900
|
-
return window.NDEFReader;
|
|
2901
|
-
}
|
|
2902
|
-
class WebNfcService extends BrowserApiBaseService {
|
|
2903
|
-
getApiName() {
|
|
2904
|
-
return 'web-nfc';
|
|
2905
|
-
}
|
|
2906
|
-
isSupported() {
|
|
2907
|
-
return this.isBrowserEnvironment() && 'NDEFReader' in window;
|
|
2908
|
-
}
|
|
2909
|
-
scan() {
|
|
2910
|
-
if (!this.isSupported()) {
|
|
2911
|
-
return new Observable((o) => o.error(new Error('Web NFC API not supported')));
|
|
2912
|
-
}
|
|
2913
|
-
return new Observable((subscriber) => {
|
|
2914
|
-
const abortController = new AbortController();
|
|
2915
|
-
const reader = new (getNdefReaderClass())();
|
|
2916
|
-
const onReading = (event) => {
|
|
2917
|
-
const e = event;
|
|
2918
|
-
subscriber.next({
|
|
2919
|
-
serialNumber: e.serialNumber,
|
|
2920
|
-
message: e.message,
|
|
2921
|
-
});
|
|
2922
|
-
};
|
|
2923
|
-
const onError = (event) => {
|
|
2924
|
-
subscriber.error(event.error ?? new Error('NFC read error'));
|
|
2925
|
-
};
|
|
2926
|
-
reader.addEventListener('reading', onReading);
|
|
2927
|
-
reader.addEventListener('readingerror', onError);
|
|
2928
|
-
reader
|
|
2929
|
-
.scan({ signal: abortController.signal })
|
|
2930
|
-
.catch((err) => subscriber.error(err));
|
|
2931
|
-
return () => abortController.abort();
|
|
2932
|
-
});
|
|
2933
|
-
}
|
|
2934
|
-
async write(message, options) {
|
|
2935
|
-
if (!this.isSupported()) {
|
|
2936
|
-
throw new Error('Web NFC API not supported');
|
|
2937
|
-
}
|
|
2938
|
-
const reader = new (getNdefReaderClass())();
|
|
2939
|
-
await reader.write(message, options);
|
|
2940
|
-
}
|
|
2941
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebNfcService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2942
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebNfcService });
|
|
2943
|
-
}
|
|
2944
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebNfcService, decorators: [{
|
|
2945
|
-
type: Injectable
|
|
2946
|
-
}] });
|
|
2947
|
-
|
|
2948
|
-
class PaymentRequestService extends BrowserApiBaseService {
|
|
2949
|
-
getApiName() {
|
|
2950
|
-
return 'payment-request';
|
|
2951
|
-
}
|
|
2952
|
-
isSupported() {
|
|
2953
|
-
return this.isBrowserEnvironment() && 'PaymentRequest' in window;
|
|
2954
|
-
}
|
|
2955
|
-
async canMakePayment(methods, details) {
|
|
2956
|
-
if (!this.isSupported())
|
|
2957
|
-
return false;
|
|
2958
|
-
const request = new PaymentRequest(methods, details);
|
|
2959
|
-
return request.canMakePayment();
|
|
2960
|
-
}
|
|
2961
|
-
async show(methods, details, options) {
|
|
2962
|
-
if (!this.isSupported()) {
|
|
2963
|
-
throw new Error('Payment Request API not supported');
|
|
2964
|
-
}
|
|
2965
|
-
const request = new PaymentRequest(methods, details, options);
|
|
2966
|
-
const response = await request.show();
|
|
2967
|
-
const result = {
|
|
2968
|
-
methodName: response.methodName,
|
|
2969
|
-
details: response.details,
|
|
2970
|
-
payerName: response.payerName ?? null,
|
|
2971
|
-
payerEmail: response.payerEmail ?? null,
|
|
2972
|
-
payerPhone: response.payerPhone ?? null,
|
|
2973
|
-
};
|
|
2974
|
-
await response.complete('success');
|
|
2975
|
-
return result;
|
|
2976
|
-
}
|
|
2977
|
-
async abort(methods, details) {
|
|
2978
|
-
if (!this.isSupported())
|
|
2979
|
-
return;
|
|
2980
|
-
const request = new PaymentRequest(methods, details);
|
|
2981
|
-
await request.abort();
|
|
2982
|
-
}
|
|
2983
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PaymentRequestService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
2984
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PaymentRequestService });
|
|
2985
|
-
}
|
|
2986
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PaymentRequestService, decorators: [{
|
|
2987
|
-
type: Injectable
|
|
2988
|
-
}] });
|
|
2989
|
-
|
|
2990
|
-
class CredentialManagementService extends BrowserApiBaseService {
|
|
2991
|
-
getApiName() {
|
|
2992
|
-
return 'credential-management';
|
|
2993
|
-
}
|
|
2994
|
-
isSupported() {
|
|
2995
|
-
return this.isBrowserEnvironment() && 'credentials' in navigator;
|
|
2996
|
-
}
|
|
2997
|
-
isPublicKeySupported() {
|
|
2998
|
-
return this.isSupported() && 'PublicKeyCredential' in window;
|
|
2999
|
-
}
|
|
3000
|
-
async get(options) {
|
|
3001
|
-
if (!this.isSupported()) {
|
|
3002
|
-
throw new Error('Credential Management API not supported');
|
|
3003
|
-
}
|
|
3004
|
-
return navigator.credentials.get(options);
|
|
3005
|
-
}
|
|
3006
|
-
async store(credential) {
|
|
3007
|
-
if (!this.isSupported()) {
|
|
3008
|
-
throw new Error('Credential Management API not supported');
|
|
3009
|
-
}
|
|
3010
|
-
await navigator.credentials.store(credential);
|
|
3011
|
-
}
|
|
3012
|
-
async createPasswordCredential(data) {
|
|
3013
|
-
if (!this.isSupported()) {
|
|
3014
|
-
throw new Error('Credential Management API not supported');
|
|
3015
|
-
}
|
|
3016
|
-
return navigator.credentials.create({
|
|
3017
|
-
password: data,
|
|
3018
|
-
});
|
|
3019
|
-
}
|
|
3020
|
-
async createPublicKeyCredential(options) {
|
|
3021
|
-
if (!this.isPublicKeySupported()) {
|
|
3022
|
-
throw new Error('PublicKeyCredential API not supported');
|
|
3023
|
-
}
|
|
3024
|
-
return navigator.credentials.create({
|
|
3025
|
-
publicKey: options,
|
|
3026
|
-
});
|
|
3027
|
-
}
|
|
3028
|
-
async preventSilentAccess() {
|
|
3029
|
-
if (!this.isSupported())
|
|
3030
|
-
return;
|
|
3031
|
-
await navigator.credentials.preventSilentAccess();
|
|
3032
|
-
}
|
|
3033
|
-
async isConditionalMediationAvailable() {
|
|
3034
|
-
if (!this.isPublicKeySupported())
|
|
3035
|
-
return false;
|
|
3036
|
-
if ('isConditionalMediationAvailable' in PublicKeyCredential) {
|
|
3037
|
-
return PublicKeyCredential.isConditionalMediationAvailable();
|
|
3038
|
-
}
|
|
3039
|
-
return false;
|
|
3040
|
-
}
|
|
3041
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CredentialManagementService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
3042
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CredentialManagementService });
|
|
3043
|
-
}
|
|
3044
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CredentialManagementService, decorators: [{
|
|
3045
|
-
type: Injectable
|
|
3046
|
-
}] });
|
|
3047
|
-
|
|
3048
2987
|
// Common types for browser APIs
|
|
3049
2988
|
|
|
3050
2989
|
function injectPageVisibility() {
|
|
@@ -3208,42 +3147,6 @@ function injectPerformanceObserver(config) {
|
|
|
3208
3147
|
};
|
|
3209
3148
|
}
|
|
3210
3149
|
|
|
3211
|
-
function getIdleDetectorClass() {
|
|
3212
|
-
return window.IdleDetector;
|
|
3213
|
-
}
|
|
3214
|
-
function injectIdleDetector(options = {}) {
|
|
3215
|
-
const destroyRef = inject(DestroyRef);
|
|
3216
|
-
const platformId = inject(PLATFORM_ID);
|
|
3217
|
-
const defaultState = { user: 'active', screen: 'unlocked' };
|
|
3218
|
-
const state = signal(defaultState, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
3219
|
-
if (isPlatformBrowser(platformId) && 'IdleDetector' in window) {
|
|
3220
|
-
const abortController = new AbortController();
|
|
3221
|
-
const detector = new (getIdleDetectorClass())();
|
|
3222
|
-
detector.addEventListener('change', () => {
|
|
3223
|
-
state.set({
|
|
3224
|
-
user: detector.userState,
|
|
3225
|
-
screen: detector.screenState,
|
|
3226
|
-
});
|
|
3227
|
-
});
|
|
3228
|
-
detector
|
|
3229
|
-
.start({
|
|
3230
|
-
threshold: options.threshold ?? 60_000,
|
|
3231
|
-
signal: abortController.signal,
|
|
3232
|
-
})
|
|
3233
|
-
.catch(() => {
|
|
3234
|
-
/* permission denied or unsupported — keep defaults */
|
|
3235
|
-
});
|
|
3236
|
-
destroyRef.onDestroy(() => abortController.abort());
|
|
3237
|
-
}
|
|
3238
|
-
return {
|
|
3239
|
-
state: state.asReadonly(),
|
|
3240
|
-
userState: computed(() => state().user),
|
|
3241
|
-
screenState: computed(() => state().screen),
|
|
3242
|
-
isUserIdle: computed(() => state().user === 'idle'),
|
|
3243
|
-
isScreenLocked: computed(() => state().screen === 'locked'),
|
|
3244
|
-
};
|
|
3245
|
-
}
|
|
3246
|
-
|
|
3247
3150
|
function injectGamepad(index, intervalMs = 16) {
|
|
3248
3151
|
const destroyRef = inject(DestroyRef);
|
|
3249
3152
|
const platformId = inject(PLATFORM_ID);
|
|
@@ -3410,10 +3313,6 @@ function provideMediaRecorder() {
|
|
|
3410
3313
|
return makeEnvironmentProviders([PermissionsService, MediaRecorderService]);
|
|
3411
3314
|
}
|
|
3412
3315
|
|
|
3413
|
-
function provideIdleDetector() {
|
|
3414
|
-
return makeEnvironmentProviders([PermissionsService, IdleDetectorService]);
|
|
3415
|
-
}
|
|
3416
|
-
|
|
3417
3316
|
function provideBattery() {
|
|
3418
3317
|
return makeEnvironmentProviders([BatteryService]);
|
|
3419
3318
|
}
|
|
@@ -3482,14 +3381,6 @@ function providePerformanceObserver() {
|
|
|
3482
3381
|
return makeEnvironmentProviders([PerformanceObserverService]);
|
|
3483
3382
|
}
|
|
3484
3383
|
|
|
3485
|
-
function provideEyeDropper() {
|
|
3486
|
-
return makeEnvironmentProviders([EyeDropperService]);
|
|
3487
|
-
}
|
|
3488
|
-
|
|
3489
|
-
function provideBarcodeDetector() {
|
|
3490
|
-
return makeEnvironmentProviders([BarcodeDetectorService]);
|
|
3491
|
-
}
|
|
3492
|
-
|
|
3493
3384
|
function provideWebAudio() {
|
|
3494
3385
|
return makeEnvironmentProviders([WebAudioService]);
|
|
3495
3386
|
}
|
|
@@ -3498,26 +3389,6 @@ function provideGamepad() {
|
|
|
3498
3389
|
return makeEnvironmentProviders([GamepadService]);
|
|
3499
3390
|
}
|
|
3500
3391
|
|
|
3501
|
-
function provideWebBluetooth() {
|
|
3502
|
-
return makeEnvironmentProviders([WebBluetoothService]);
|
|
3503
|
-
}
|
|
3504
|
-
|
|
3505
|
-
function provideWebUsb() {
|
|
3506
|
-
return makeEnvironmentProviders([WebUsbService]);
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
|
-
function provideWebNfc() {
|
|
3510
|
-
return makeEnvironmentProviders([WebNfcService]);
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
function providePaymentRequest() {
|
|
3514
|
-
return makeEnvironmentProviders([PaymentRequestService]);
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
function provideCredentialManagement() {
|
|
3518
|
-
return makeEnvironmentProviders([CredentialManagementService]);
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
3392
|
function provideMediaApis() {
|
|
3522
3393
|
return makeEnvironmentProviders([PermissionsService, CameraService, MediaDevicesService]);
|
|
3523
3394
|
}
|
|
@@ -3562,16 +3433,8 @@ const defaultBrowserWebApisConfig = {
|
|
|
3562
3433
|
enableSpeechSynthesis: false,
|
|
3563
3434
|
enableMutationObserver: false,
|
|
3564
3435
|
enablePerformanceObserver: false,
|
|
3565
|
-
enableIdleDetector: false,
|
|
3566
|
-
enableEyeDropper: false,
|
|
3567
|
-
enableBarcodeDetector: false,
|
|
3568
3436
|
enableWebAudio: false,
|
|
3569
3437
|
enableGamepad: false,
|
|
3570
|
-
enableWebBluetooth: false,
|
|
3571
|
-
enableWebUsb: false,
|
|
3572
|
-
enableWebNfc: false,
|
|
3573
|
-
enablePaymentRequest: false,
|
|
3574
|
-
enableCredentialManagement: false,
|
|
3575
3438
|
};
|
|
3576
3439
|
function provideBrowserWebApis(config = {}) {
|
|
3577
3440
|
const mergedConfig = { ...defaultBrowserWebApisConfig, ...config };
|
|
@@ -3602,16 +3465,8 @@ function provideBrowserWebApis(config = {}) {
|
|
|
3602
3465
|
[mergedConfig.enableSpeechSynthesis, SpeechSynthesisService],
|
|
3603
3466
|
[mergedConfig.enableMutationObserver, MutationObserverService],
|
|
3604
3467
|
[mergedConfig.enablePerformanceObserver, PerformanceObserverService],
|
|
3605
|
-
[mergedConfig.enableIdleDetector, IdleDetectorService],
|
|
3606
|
-
[mergedConfig.enableEyeDropper, EyeDropperService],
|
|
3607
|
-
[mergedConfig.enableBarcodeDetector, BarcodeDetectorService],
|
|
3608
3468
|
[mergedConfig.enableWebAudio, WebAudioService],
|
|
3609
3469
|
[mergedConfig.enableGamepad, GamepadService],
|
|
3610
|
-
[mergedConfig.enableWebBluetooth, WebBluetoothService],
|
|
3611
|
-
[mergedConfig.enableWebUsb, WebUsbService],
|
|
3612
|
-
[mergedConfig.enableWebNfc, WebNfcService],
|
|
3613
|
-
[mergedConfig.enablePaymentRequest, PaymentRequestService],
|
|
3614
|
-
[mergedConfig.enableCredentialManagement, CredentialManagementService],
|
|
3615
3470
|
];
|
|
3616
3471
|
for (const [enabled, provider] of conditionalProviders) {
|
|
3617
3472
|
if (enabled) {
|
|
@@ -3629,4 +3484,4 @@ const version = '0.1.0';
|
|
|
3629
3484
|
* Generated bundle index. Do not edit.
|
|
3630
3485
|
*/
|
|
3631
3486
|
|
|
3632
|
-
export { BROWSER_API_LOGGER,
|
|
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 };
|