- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
704 lines
25 KiB
JavaScript
704 lines
25 KiB
JavaScript
// modules/api-manager/DeviceManager.js
|
|
import { Logger } from '../../core/logger.js';
|
|
|
|
/**
|
|
* Device APIs Manager - Geolocation, Sensors, Battery, Network, Vibration
|
|
*/
|
|
export class DeviceManager {
|
|
constructor(config = {}) {
|
|
this.config = config;
|
|
this.activeWatchers = new Map();
|
|
this.sensorData = new Map();
|
|
|
|
// Check API support
|
|
this.support = {
|
|
geolocation: 'geolocation' in navigator,
|
|
deviceMotion: 'DeviceMotionEvent' in window,
|
|
deviceOrientation: 'DeviceOrientationEvent' in window,
|
|
vibration: 'vibrate' in navigator,
|
|
battery: 'getBattery' in navigator,
|
|
networkInfo: 'connection' in navigator || 'mozConnection' in navigator || 'webkitConnection' in navigator,
|
|
wakeLock: 'wakeLock' in navigator,
|
|
bluetooth: 'bluetooth' in navigator,
|
|
usb: 'usb' in navigator,
|
|
serial: 'serial' in navigator
|
|
};
|
|
|
|
Logger.info('[DeviceManager] Initialized with support:', this.support);
|
|
|
|
// Initialize sensors if available
|
|
this.initializeSensors();
|
|
}
|
|
|
|
/**
|
|
* Geolocation API
|
|
*/
|
|
geolocation = {
|
|
// Get current position
|
|
getCurrent: (options = {}) => {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.support.geolocation) {
|
|
reject(new Error('Geolocation not supported'));
|
|
return;
|
|
}
|
|
|
|
const defaultOptions = {
|
|
enableHighAccuracy: true,
|
|
timeout: 10000,
|
|
maximumAge: 60000,
|
|
...options
|
|
};
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
const location = this.enhanceLocationData(position);
|
|
Logger.info('[DeviceManager] Location acquired:', {
|
|
lat: location.latitude,
|
|
lng: location.longitude,
|
|
accuracy: location.accuracy
|
|
});
|
|
resolve(location);
|
|
},
|
|
(error) => {
|
|
Logger.error('[DeviceManager] Geolocation failed:', error.message);
|
|
reject(error);
|
|
},
|
|
defaultOptions
|
|
);
|
|
});
|
|
},
|
|
|
|
// Watch position changes
|
|
watch: (callback, options = {}) => {
|
|
if (!this.support.geolocation) {
|
|
throw new Error('Geolocation not supported');
|
|
}
|
|
|
|
const defaultOptions = {
|
|
enableHighAccuracy: true,
|
|
timeout: 30000,
|
|
maximumAge: 10000,
|
|
...options
|
|
};
|
|
|
|
const watchId = navigator.geolocation.watchPosition(
|
|
(position) => {
|
|
const location = this.enhanceLocationData(position);
|
|
callback(location);
|
|
},
|
|
(error) => {
|
|
Logger.error('[DeviceManager] Location watch failed:', error.message);
|
|
callback({ error });
|
|
},
|
|
defaultOptions
|
|
);
|
|
|
|
this.activeWatchers.set(`geo_${watchId}`, {
|
|
type: 'geolocation',
|
|
id: watchId,
|
|
stop: () => {
|
|
navigator.geolocation.clearWatch(watchId);
|
|
this.activeWatchers.delete(`geo_${watchId}`);
|
|
}
|
|
});
|
|
|
|
Logger.info('[DeviceManager] Location watch started:', watchId);
|
|
|
|
return {
|
|
id: watchId,
|
|
stop: () => {
|
|
navigator.geolocation.clearWatch(watchId);
|
|
this.activeWatchers.delete(`geo_${watchId}`);
|
|
Logger.info('[DeviceManager] Location watch stopped:', watchId);
|
|
}
|
|
};
|
|
},
|
|
|
|
// Calculate distance between two points
|
|
distance: (pos1, pos2) => {
|
|
const R = 6371; // Earth's radius in km
|
|
const dLat = this.toRadians(pos2.latitude - pos1.latitude);
|
|
const dLon = this.toRadians(pos2.longitude - pos1.longitude);
|
|
|
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(this.toRadians(pos1.latitude)) * Math.cos(this.toRadians(pos2.latitude)) *
|
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
const distance = R * c;
|
|
|
|
return {
|
|
kilometers: distance,
|
|
miles: distance * 0.621371,
|
|
meters: distance * 1000
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Device Motion and Orientation
|
|
*/
|
|
motion = {
|
|
// Start motion detection
|
|
start: (callback, options = {}) => {
|
|
if (!this.support.deviceMotion) {
|
|
throw new Error('Device Motion not supported');
|
|
}
|
|
|
|
const handler = (event) => {
|
|
const motionData = {
|
|
acceleration: event.acceleration,
|
|
accelerationIncludingGravity: event.accelerationIncludingGravity,
|
|
rotationRate: event.rotationRate,
|
|
interval: event.interval,
|
|
timestamp: event.timeStamp,
|
|
// Enhanced data
|
|
totalAcceleration: this.calculateTotalAcceleration(event.acceleration),
|
|
shake: this.detectShake(event.accelerationIncludingGravity),
|
|
orientation: this.getDeviceOrientation(event)
|
|
};
|
|
|
|
callback(motionData);
|
|
};
|
|
|
|
// Request permission for iOS 13+
|
|
if (typeof DeviceMotionEvent.requestPermission === 'function') {
|
|
DeviceMotionEvent.requestPermission().then(response => {
|
|
if (response === 'granted') {
|
|
window.addEventListener('devicemotion', handler);
|
|
}
|
|
});
|
|
} else {
|
|
window.addEventListener('devicemotion', handler);
|
|
}
|
|
|
|
const watcherId = this.generateId('motion');
|
|
this.activeWatchers.set(watcherId, {
|
|
type: 'motion',
|
|
handler,
|
|
stop: () => {
|
|
window.removeEventListener('devicemotion', handler);
|
|
this.activeWatchers.delete(watcherId);
|
|
}
|
|
});
|
|
|
|
Logger.info('[DeviceManager] Motion detection started');
|
|
|
|
return {
|
|
id: watcherId,
|
|
stop: () => {
|
|
window.removeEventListener('devicemotion', handler);
|
|
this.activeWatchers.delete(watcherId);
|
|
Logger.info('[DeviceManager] Motion detection stopped');
|
|
}
|
|
};
|
|
},
|
|
|
|
// Start orientation detection
|
|
startOrientation: (callback, options = {}) => {
|
|
if (!this.support.deviceOrientation) {
|
|
throw new Error('Device Orientation not supported');
|
|
}
|
|
|
|
const handler = (event) => {
|
|
const orientationData = {
|
|
alpha: event.alpha, // Z axis (0-360)
|
|
beta: event.beta, // X axis (-180 to 180)
|
|
gamma: event.gamma, // Y axis (-90 to 90)
|
|
absolute: event.absolute,
|
|
timestamp: event.timeStamp,
|
|
// Enhanced data
|
|
compass: this.calculateCompass(event.alpha),
|
|
tilt: this.calculateTilt(event.beta, event.gamma),
|
|
rotation: this.getRotationState(event)
|
|
};
|
|
|
|
callback(orientationData);
|
|
};
|
|
|
|
// Request permission for iOS 13+
|
|
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
|
|
DeviceOrientationEvent.requestPermission().then(response => {
|
|
if (response === 'granted') {
|
|
window.addEventListener('deviceorientation', handler);
|
|
}
|
|
});
|
|
} else {
|
|
window.addEventListener('deviceorientation', handler);
|
|
}
|
|
|
|
const watcherId = this.generateId('orientation');
|
|
this.activeWatchers.set(watcherId, {
|
|
type: 'orientation',
|
|
handler,
|
|
stop: () => {
|
|
window.removeEventListener('deviceorientation', handler);
|
|
this.activeWatchers.delete(watcherId);
|
|
}
|
|
});
|
|
|
|
Logger.info('[DeviceManager] Orientation detection started');
|
|
|
|
return {
|
|
id: watcherId,
|
|
stop: () => {
|
|
window.removeEventListener('deviceorientation', handler);
|
|
this.activeWatchers.delete(watcherId);
|
|
Logger.info('[DeviceManager] Orientation detection stopped');
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Vibration API
|
|
*/
|
|
vibration = {
|
|
// Simple vibration
|
|
vibrate: (pattern) => {
|
|
if (!this.support.vibration) {
|
|
Logger.warn('[DeviceManager] Vibration not supported');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
navigator.vibrate(pattern);
|
|
Logger.info('[DeviceManager] Vibration triggered:', pattern);
|
|
return true;
|
|
} catch (error) {
|
|
Logger.error('[DeviceManager] Vibration failed:', error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// Predefined patterns
|
|
patterns: {
|
|
short: 200,
|
|
long: 600,
|
|
double: [200, 100, 200],
|
|
triple: [200, 100, 200, 100, 200],
|
|
sos: [100, 30, 100, 30, 100, 200, 200, 30, 200, 30, 200, 200, 100, 30, 100, 30, 100],
|
|
heartbeat: [100, 30, 100, 130, 40, 30, 40, 30, 100],
|
|
notification: [200, 100, 200],
|
|
success: [100],
|
|
error: [300, 100, 300],
|
|
warning: [200, 100, 200, 100, 200]
|
|
},
|
|
|
|
// Stop vibration
|
|
stop: () => {
|
|
if (this.support.vibration) {
|
|
navigator.vibrate(0);
|
|
Logger.info('[DeviceManager] Vibration stopped');
|
|
}
|
|
},
|
|
|
|
// Haptic feedback helpers
|
|
success: () => this.vibration.vibrate(this.vibration.patterns.success),
|
|
error: () => this.vibration.vibrate(this.vibration.patterns.error),
|
|
warning: () => this.vibration.vibrate(this.vibration.patterns.warning),
|
|
notification: () => this.vibration.vibrate(this.vibration.patterns.notification)
|
|
};
|
|
|
|
/**
|
|
* Battery API
|
|
*/
|
|
battery = {
|
|
// Get battery status
|
|
get: async () => {
|
|
if (!this.support.battery) {
|
|
Logger.warn('[DeviceManager] Battery API not supported');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const battery = await navigator.getBattery();
|
|
|
|
const batteryInfo = {
|
|
level: Math.round(battery.level * 100),
|
|
charging: battery.charging,
|
|
chargingTime: battery.chargingTime,
|
|
dischargingTime: battery.dischargingTime,
|
|
// Enhanced data
|
|
status: this.getBatteryStatus(battery),
|
|
timeRemaining: this.formatBatteryTime(battery)
|
|
};
|
|
|
|
Logger.info('[DeviceManager] Battery status:', batteryInfo);
|
|
return batteryInfo;
|
|
} catch (error) {
|
|
Logger.error('[DeviceManager] Battery status failed:', error);
|
|
return null;
|
|
}
|
|
},
|
|
|
|
// Watch battery changes
|
|
watch: async (callback) => {
|
|
if (!this.support.battery) {
|
|
throw new Error('Battery API not supported');
|
|
}
|
|
|
|
try {
|
|
const battery = await navigator.getBattery();
|
|
|
|
const events = ['chargingchange', 'levelchange', 'chargingtimechange', 'dischargingtimechange'];
|
|
const handlers = [];
|
|
|
|
events.forEach(eventType => {
|
|
const handler = () => {
|
|
const batteryInfo = {
|
|
level: Math.round(battery.level * 100),
|
|
charging: battery.charging,
|
|
chargingTime: battery.chargingTime,
|
|
dischargingTime: battery.dischargingTime,
|
|
status: this.getBatteryStatus(battery),
|
|
timeRemaining: this.formatBatteryTime(battery),
|
|
event: eventType
|
|
};
|
|
callback(batteryInfo);
|
|
};
|
|
|
|
battery.addEventListener(eventType, handler);
|
|
handlers.push({ event: eventType, handler });
|
|
});
|
|
|
|
const watcherId = this.generateId('battery');
|
|
this.activeWatchers.set(watcherId, {
|
|
type: 'battery',
|
|
battery,
|
|
handlers,
|
|
stop: () => {
|
|
handlers.forEach(({ event, handler }) => {
|
|
battery.removeEventListener(event, handler);
|
|
});
|
|
this.activeWatchers.delete(watcherId);
|
|
}
|
|
});
|
|
|
|
Logger.info('[DeviceManager] Battery watch started');
|
|
|
|
return {
|
|
id: watcherId,
|
|
stop: () => {
|
|
handlers.forEach(({ event, handler }) => {
|
|
battery.removeEventListener(event, handler);
|
|
});
|
|
this.activeWatchers.delete(watcherId);
|
|
Logger.info('[DeviceManager] Battery watch stopped');
|
|
}
|
|
};
|
|
} catch (error) {
|
|
Logger.error('[DeviceManager] Battery watch failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Network Information API
|
|
*/
|
|
network = {
|
|
// Get connection info
|
|
get: () => {
|
|
if (!this.support.networkInfo) {
|
|
Logger.warn('[DeviceManager] Network Information not supported');
|
|
return null;
|
|
}
|
|
|
|
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
|
|
return {
|
|
effectiveType: connection.effectiveType,
|
|
downlink: connection.downlink,
|
|
rtt: connection.rtt,
|
|
saveData: connection.saveData,
|
|
// Enhanced data
|
|
speed: this.getConnectionSpeed(connection),
|
|
quality: this.getConnectionQuality(connection),
|
|
recommendation: this.getNetworkRecommendation(connection)
|
|
};
|
|
},
|
|
|
|
// Watch network changes
|
|
watch: (callback) => {
|
|
if (!this.support.networkInfo) {
|
|
throw new Error('Network Information not supported');
|
|
}
|
|
|
|
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
|
|
const handler = () => {
|
|
const networkInfo = {
|
|
effectiveType: connection.effectiveType,
|
|
downlink: connection.downlink,
|
|
rtt: connection.rtt,
|
|
saveData: connection.saveData,
|
|
speed: this.getConnectionSpeed(connection),
|
|
quality: this.getConnectionQuality(connection),
|
|
recommendation: this.getNetworkRecommendation(connection)
|
|
};
|
|
|
|
callback(networkInfo);
|
|
};
|
|
|
|
connection.addEventListener('change', handler);
|
|
|
|
const watcherId = this.generateId('network');
|
|
this.activeWatchers.set(watcherId, {
|
|
type: 'network',
|
|
connection,
|
|
handler,
|
|
stop: () => {
|
|
connection.removeEventListener('change', handler);
|
|
this.activeWatchers.delete(watcherId);
|
|
}
|
|
});
|
|
|
|
Logger.info('[DeviceManager] Network watch started');
|
|
|
|
return {
|
|
id: watcherId,
|
|
stop: () => {
|
|
connection.removeEventListener('change', handler);
|
|
this.activeWatchers.delete(watcherId);
|
|
Logger.info('[DeviceManager] Network watch stopped');
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Wake Lock API
|
|
*/
|
|
wakeLock = {
|
|
// Request wake lock
|
|
request: async (type = 'screen') => {
|
|
if (!this.support.wakeLock) {
|
|
Logger.warn('[DeviceManager] Wake Lock not supported');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const wakeLock = await navigator.wakeLock.request(type);
|
|
Logger.info(`[DeviceManager] Wake lock acquired: ${type}`);
|
|
|
|
return {
|
|
type: wakeLock.type,
|
|
release: () => {
|
|
wakeLock.release();
|
|
Logger.info(`[DeviceManager] Wake lock released: ${type}`);
|
|
}
|
|
};
|
|
} catch (error) {
|
|
Logger.error('[DeviceManager] Wake lock failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Helper methods
|
|
|
|
initializeSensors() {
|
|
// Initialize any sensors that need setup
|
|
if (this.support.deviceMotion || this.support.deviceOrientation) {
|
|
// Store baseline sensor data for comparison
|
|
this.sensorData.set('motionBaseline', { x: 0, y: 0, z: 0 });
|
|
this.sensorData.set('shakeThreshold', this.config.shakeThreshold || 15);
|
|
}
|
|
}
|
|
|
|
enhanceLocationData(position) {
|
|
return {
|
|
latitude: position.coords.latitude,
|
|
longitude: position.coords.longitude,
|
|
accuracy: position.coords.accuracy,
|
|
altitude: position.coords.altitude,
|
|
altitudeAccuracy: position.coords.altitudeAccuracy,
|
|
heading: position.coords.heading,
|
|
speed: position.coords.speed,
|
|
timestamp: position.timestamp,
|
|
// Enhanced data
|
|
coordinates: `${position.coords.latitude},${position.coords.longitude}`,
|
|
accuracyLevel: this.getAccuracyLevel(position.coords.accuracy),
|
|
mapUrl: `https://maps.google.com/?q=${position.coords.latitude},${position.coords.longitude}`
|
|
};
|
|
}
|
|
|
|
getAccuracyLevel(accuracy) {
|
|
if (accuracy <= 5) return 'excellent';
|
|
if (accuracy <= 10) return 'good';
|
|
if (accuracy <= 50) return 'fair';
|
|
return 'poor';
|
|
}
|
|
|
|
calculateTotalAcceleration(acceleration) {
|
|
if (!acceleration) return 0;
|
|
const x = acceleration.x || 0;
|
|
const y = acceleration.y || 0;
|
|
const z = acceleration.z || 0;
|
|
return Math.sqrt(x * x + y * y + z * z);
|
|
}
|
|
|
|
detectShake(acceleration) {
|
|
if (!acceleration) return false;
|
|
|
|
const threshold = this.sensorData.get('shakeThreshold');
|
|
const x = Math.abs(acceleration.x || 0);
|
|
const y = Math.abs(acceleration.y || 0);
|
|
const z = Math.abs(acceleration.z || 0);
|
|
|
|
return (x > threshold || y > threshold || z > threshold);
|
|
}
|
|
|
|
getDeviceOrientation(event) {
|
|
const acceleration = event.accelerationIncludingGravity;
|
|
if (!acceleration) return 'unknown';
|
|
|
|
const x = acceleration.x || 0;
|
|
const y = acceleration.y || 0;
|
|
const z = acceleration.z || 0;
|
|
|
|
if (Math.abs(x) > Math.abs(y) && Math.abs(x) > Math.abs(z)) {
|
|
return x > 0 ? 'landscape-right' : 'landscape-left';
|
|
} else if (Math.abs(y) > Math.abs(z)) {
|
|
return y > 0 ? 'portrait-upside-down' : 'portrait';
|
|
} else {
|
|
return z > 0 ? 'face-down' : 'face-up';
|
|
}
|
|
}
|
|
|
|
calculateCompass(alpha) {
|
|
if (alpha === null) return null;
|
|
|
|
const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
|
|
const index = Math.round(alpha / 45) % 8;
|
|
|
|
return {
|
|
degrees: Math.round(alpha),
|
|
direction: directions[index],
|
|
cardinal: this.getCardinalDirection(alpha)
|
|
};
|
|
}
|
|
|
|
getCardinalDirection(alpha) {
|
|
if (alpha >= 337.5 || alpha < 22.5) return 'North';
|
|
if (alpha >= 22.5 && alpha < 67.5) return 'Northeast';
|
|
if (alpha >= 67.5 && alpha < 112.5) return 'East';
|
|
if (alpha >= 112.5 && alpha < 157.5) return 'Southeast';
|
|
if (alpha >= 157.5 && alpha < 202.5) return 'South';
|
|
if (alpha >= 202.5 && alpha < 247.5) return 'Southwest';
|
|
if (alpha >= 247.5 && alpha < 292.5) return 'West';
|
|
if (alpha >= 292.5 && alpha < 337.5) return 'Northwest';
|
|
return 'Unknown';
|
|
}
|
|
|
|
calculateTilt(beta, gamma) {
|
|
return {
|
|
x: Math.round(beta || 0),
|
|
y: Math.round(gamma || 0),
|
|
magnitude: Math.round(Math.sqrt((beta || 0) ** 2 + (gamma || 0) ** 2))
|
|
};
|
|
}
|
|
|
|
getRotationState(event) {
|
|
const { alpha, beta, gamma } = event;
|
|
|
|
// Determine if device is being rotated significantly
|
|
const rotationThreshold = 10;
|
|
const isRotating = Math.abs(beta) > rotationThreshold || Math.abs(gamma) > rotationThreshold;
|
|
|
|
return {
|
|
isRotating,
|
|
intensity: isRotating ? Math.max(Math.abs(beta), Math.abs(gamma)) : 0
|
|
};
|
|
}
|
|
|
|
getBatteryStatus(battery) {
|
|
const level = battery.level * 100;
|
|
|
|
if (battery.charging) return 'charging';
|
|
if (level <= 10) return 'critical';
|
|
if (level <= 20) return 'low';
|
|
if (level <= 50) return 'medium';
|
|
return 'high';
|
|
}
|
|
|
|
formatBatteryTime(battery) {
|
|
const time = battery.charging ? battery.chargingTime : battery.dischargingTime;
|
|
|
|
if (time === Infinity || isNaN(time)) return 'Unknown';
|
|
|
|
const hours = Math.floor(time / 3600);
|
|
const minutes = Math.floor((time % 3600) / 60);
|
|
|
|
return `${hours}h ${minutes}m`;
|
|
}
|
|
|
|
getConnectionSpeed(connection) {
|
|
const downlink = connection.downlink;
|
|
|
|
if (downlink >= 10) return 'fast';
|
|
if (downlink >= 1.5) return 'good';
|
|
if (downlink >= 0.5) return 'slow';
|
|
return 'very-slow';
|
|
}
|
|
|
|
getConnectionQuality(connection) {
|
|
const effectiveType = connection.effectiveType;
|
|
|
|
switch (effectiveType) {
|
|
case '4g': return 'excellent';
|
|
case '3g': return 'good';
|
|
case '2g': return 'poor';
|
|
case 'slow-2g': return 'very-poor';
|
|
default: return 'unknown';
|
|
}
|
|
}
|
|
|
|
getNetworkRecommendation(connection) {
|
|
const quality = this.getConnectionQuality(connection);
|
|
|
|
switch (quality) {
|
|
case 'excellent':
|
|
return 'Full quality content recommended';
|
|
case 'good':
|
|
return 'Moderate quality content recommended';
|
|
case 'poor':
|
|
return 'Light content only, avoid large files';
|
|
case 'very-poor':
|
|
return 'Text-only content recommended';
|
|
default:
|
|
return 'Monitor connection quality';
|
|
}
|
|
}
|
|
|
|
toRadians(degrees) {
|
|
return degrees * (Math.PI / 180);
|
|
}
|
|
|
|
generateId(prefix = 'device') {
|
|
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
/**
|
|
* Stop all active watchers
|
|
*/
|
|
stopAllWatchers() {
|
|
this.activeWatchers.forEach(watcher => {
|
|
watcher.stop();
|
|
});
|
|
this.activeWatchers.clear();
|
|
Logger.info('[DeviceManager] All watchers stopped');
|
|
}
|
|
|
|
/**
|
|
* Get device capabilities summary
|
|
*/
|
|
getCapabilities() {
|
|
return {
|
|
support: this.support,
|
|
activeWatchers: this.activeWatchers.size,
|
|
watcherTypes: Array.from(this.activeWatchers.values()).map(w => w.type)
|
|
};
|
|
}
|
|
} |