Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
687
resources/js/modules/api-manager/PermissionManager.js
Normal file
687
resources/js/modules/api-manager/PermissionManager.js
Normal file
@@ -0,0 +1,687 @@
|
||||
// modules/api-manager/PermissionManager.js
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Permission Management System for Web APIs
|
||||
*/
|
||||
export class PermissionManager {
|
||||
constructor(config = {}) {
|
||||
this.config = config;
|
||||
this.permissionCache = new Map();
|
||||
this.permissionWatchers = new Map();
|
||||
this.requestQueue = new Map();
|
||||
|
||||
// Check Permissions API support
|
||||
this.support = {
|
||||
permissions: 'permissions' in navigator,
|
||||
query: navigator.permissions?.query !== undefined,
|
||||
request: 'requestPermission' in Notification || 'getUserMedia' in (navigator.mediaDevices || {}),
|
||||
geolocation: 'geolocation' in navigator,
|
||||
notifications: 'Notification' in window,
|
||||
camera: navigator.mediaDevices !== undefined,
|
||||
microphone: navigator.mediaDevices !== undefined,
|
||||
clipboard: 'clipboard' in navigator,
|
||||
vibration: 'vibrate' in navigator
|
||||
};
|
||||
|
||||
// Permission mappings for different APIs
|
||||
this.permissionMap = {
|
||||
camera: { name: 'camera', api: 'mediaDevices' },
|
||||
microphone: { name: 'microphone', api: 'mediaDevices' },
|
||||
geolocation: { name: 'geolocation', api: 'geolocation' },
|
||||
notifications: { name: 'notifications', api: 'notification' },
|
||||
'clipboard-read': { name: 'clipboard-read', api: 'clipboard' },
|
||||
'clipboard-write': { name: 'clipboard-write', api: 'clipboard' },
|
||||
'background-sync': { name: 'background-sync', api: 'serviceWorker' },
|
||||
'persistent-storage': { name: 'persistent-storage', api: 'storage' },
|
||||
'push': { name: 'push', api: 'serviceWorker' },
|
||||
'midi': { name: 'midi', api: 'midi' },
|
||||
'payment-handler': { name: 'payment-handler', api: 'payment' }
|
||||
};
|
||||
|
||||
Logger.info('[PermissionManager] Initialized with support:', this.support);
|
||||
|
||||
// Auto-cache current permissions
|
||||
this.cacheCurrentPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permission status for a specific permission
|
||||
*/
|
||||
async check(permission) {
|
||||
if (!this.support.permissions || !this.support.query) {
|
||||
Logger.warn('[PermissionManager] Permissions API not supported, using fallback');
|
||||
return this.checkFallback(permission);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check cache first
|
||||
const cached = this.permissionCache.get(permission);
|
||||
if (cached && Date.now() - cached.timestamp < 30000) { // 30s cache
|
||||
return cached.status;
|
||||
}
|
||||
|
||||
const permissionDescriptor = this.getPermissionDescriptor(permission);
|
||||
const status = await navigator.permissions.query(permissionDescriptor);
|
||||
|
||||
const result = {
|
||||
name: permission,
|
||||
state: status.state,
|
||||
timestamp: Date.now(),
|
||||
supported: true
|
||||
};
|
||||
|
||||
this.permissionCache.set(permission, result);
|
||||
|
||||
// Watch for changes
|
||||
status.addEventListener('change', () => {
|
||||
this.onPermissionChange(permission, status.state);
|
||||
});
|
||||
|
||||
Logger.info(`[PermissionManager] Permission checked: ${permission} = ${status.state}`);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
Logger.warn(`[PermissionManager] Permission check failed for ${permission}:`, error.message);
|
||||
return this.checkFallback(permission);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request permission for a specific API
|
||||
*/
|
||||
async request(permission, options = {}) {
|
||||
const {
|
||||
showRationale = true,
|
||||
fallbackMessage = null,
|
||||
timeout = 30000
|
||||
} = options;
|
||||
|
||||
try {
|
||||
// Check current status first
|
||||
const currentStatus = await this.check(permission);
|
||||
if (currentStatus.state === 'granted') {
|
||||
return { granted: true, state: 'granted', fromCache: true };
|
||||
}
|
||||
|
||||
if (currentStatus.state === 'denied') {
|
||||
if (showRationale) {
|
||||
await this.showPermissionRationale(permission, fallbackMessage);
|
||||
}
|
||||
return { granted: false, state: 'denied', reason: 'previously-denied' };
|
||||
}
|
||||
|
||||
// Add to request queue to prevent multiple simultaneous requests
|
||||
const queueKey = permission;
|
||||
if (this.requestQueue.has(queueKey)) {
|
||||
Logger.info(`[PermissionManager] Permission request already in progress: ${permission}`);
|
||||
return this.requestQueue.get(queueKey);
|
||||
}
|
||||
|
||||
const requestPromise = this.executePermissionRequest(permission, timeout);
|
||||
this.requestQueue.set(queueKey, requestPromise);
|
||||
|
||||
const result = await requestPromise;
|
||||
this.requestQueue.delete(queueKey);
|
||||
|
||||
// Update cache
|
||||
this.permissionCache.set(permission, {
|
||||
name: permission,
|
||||
state: result.state,
|
||||
timestamp: Date.now(),
|
||||
supported: true
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
Logger.error(`[PermissionManager] Permission request failed: ${permission}`, error);
|
||||
this.requestQueue.delete(permission);
|
||||
return { granted: false, state: 'error', error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request multiple permissions at once
|
||||
*/
|
||||
async requestMultiple(permissions, options = {}) {
|
||||
const results = {};
|
||||
|
||||
if (options.sequential) {
|
||||
// Request permissions one by one
|
||||
for (const permission of permissions) {
|
||||
results[permission] = await this.request(permission, options);
|
||||
|
||||
// Stop if any critical permission is denied
|
||||
if (options.requireAll && !results[permission].granted) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Request permissions in parallel
|
||||
const requests = permissions.map(permission =>
|
||||
this.request(permission, options).then(result => [permission, result])
|
||||
);
|
||||
|
||||
const responses = await Promise.allSettled(requests);
|
||||
responses.forEach(response => {
|
||||
if (response.status === 'fulfilled') {
|
||||
const [permission, result] = response.value;
|
||||
results[permission] = result;
|
||||
} else {
|
||||
Logger.error('[PermissionManager] Batch permission request failed:', response.reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const summary = {
|
||||
results,
|
||||
granted: Object.values(results).filter(r => r.granted).length,
|
||||
denied: Object.values(results).filter(r => !r.granted).length,
|
||||
total: Object.keys(results).length
|
||||
};
|
||||
|
||||
Logger.info(`[PermissionManager] Batch permissions: ${summary.granted}/${summary.total} granted`);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all required permissions are granted
|
||||
*/
|
||||
async checkRequired(requiredPermissions) {
|
||||
const statuses = {};
|
||||
const missing = [];
|
||||
|
||||
for (const permission of requiredPermissions) {
|
||||
const status = await this.check(permission);
|
||||
statuses[permission] = status;
|
||||
|
||||
if (status.state !== 'granted') {
|
||||
missing.push(permission);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allGranted: missing.length === 0,
|
||||
granted: Object.keys(statuses).filter(p => statuses[p].state === 'granted'),
|
||||
missing,
|
||||
statuses
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch permission changes
|
||||
*/
|
||||
watch(permission, callback) {
|
||||
const watcherId = this.generateId('watcher');
|
||||
|
||||
this.permissionWatchers.set(watcherId, {
|
||||
permission,
|
||||
callback,
|
||||
active: true
|
||||
});
|
||||
|
||||
// Set up the watcher
|
||||
this.setupPermissionWatcher(permission, callback);
|
||||
|
||||
Logger.info(`[PermissionManager] Permission watcher created: ${permission}`);
|
||||
|
||||
return {
|
||||
id: watcherId,
|
||||
stop: () => {
|
||||
const watcher = this.permissionWatchers.get(watcherId);
|
||||
if (watcher) {
|
||||
watcher.active = false;
|
||||
this.permissionWatchers.delete(watcherId);
|
||||
Logger.info(`[PermissionManager] Permission watcher stopped: ${permission}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permission recommendations based on app features
|
||||
*/
|
||||
getRecommendations(features = []) {
|
||||
const recommendations = {
|
||||
essential: [],
|
||||
recommended: [],
|
||||
optional: []
|
||||
};
|
||||
|
||||
const featurePermissionMap = {
|
||||
camera: { permissions: ['camera'], priority: 'essential' },
|
||||
microphone: { permissions: ['microphone'], priority: 'essential' },
|
||||
location: { permissions: ['geolocation'], priority: 'recommended' },
|
||||
notifications: { permissions: ['notifications'], priority: 'recommended' },
|
||||
clipboard: { permissions: ['clipboard-read', 'clipboard-write'], priority: 'optional' },
|
||||
offline: { permissions: ['persistent-storage'], priority: 'recommended' },
|
||||
background: { permissions: ['background-sync', 'push'], priority: 'optional' }
|
||||
};
|
||||
|
||||
features.forEach(feature => {
|
||||
const mapping = featurePermissionMap[feature];
|
||||
if (mapping) {
|
||||
recommendations[mapping.priority].push(...mapping.permissions);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove duplicates
|
||||
Object.keys(recommendations).forEach(key => {
|
||||
recommendations[key] = [...new Set(recommendations[key])];
|
||||
});
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create permission onboarding flow
|
||||
*/
|
||||
createOnboardingFlow(permissions, options = {}) {
|
||||
const {
|
||||
title = 'Permissions Required',
|
||||
descriptions = {},
|
||||
onComplete = null,
|
||||
onSkip = null
|
||||
} = options;
|
||||
|
||||
return {
|
||||
permissions,
|
||||
title,
|
||||
descriptions,
|
||||
|
||||
async start() {
|
||||
Logger.info('[PermissionManager] Starting onboarding flow');
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const permission of permissions) {
|
||||
const description = descriptions[permission] || this.getDefaultDescription(permission);
|
||||
|
||||
// Show permission explanation
|
||||
const userChoice = await this.showPermissionDialog(permission, description);
|
||||
|
||||
if (userChoice === 'grant') {
|
||||
const result = await this.request(permission);
|
||||
results.push({ permission, ...result });
|
||||
} else if (userChoice === 'skip') {
|
||||
results.push({ permission, granted: false, skipped: true });
|
||||
} else {
|
||||
// User cancelled the entire flow
|
||||
if (onSkip) onSkip(results);
|
||||
return { cancelled: true, results };
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
completed: true,
|
||||
granted: results.filter(r => r.granted).length,
|
||||
total: results.length,
|
||||
results
|
||||
};
|
||||
|
||||
if (onComplete) onComplete(summary);
|
||||
return summary;
|
||||
},
|
||||
|
||||
getDefaultDescription(permission) {
|
||||
const descriptions = {
|
||||
camera: 'Take photos and record videos for enhanced functionality',
|
||||
microphone: 'Record audio for voice features and communication',
|
||||
geolocation: 'Provide location-based services and content',
|
||||
notifications: 'Send you important updates and reminders',
|
||||
'clipboard-read': 'Access clipboard content for convenience features',
|
||||
'clipboard-write': 'Copy content to clipboard for easy sharing'
|
||||
};
|
||||
|
||||
return descriptions[permission] || `Allow access to ${permission} functionality`;
|
||||
},
|
||||
|
||||
async showPermissionDialog(permission, description) {
|
||||
return new Promise(resolve => {
|
||||
// Create modal dialog
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'permission-dialog-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="permission-dialog-backdrop" style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
">
|
||||
<div class="permission-dialog" style="
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
max-width: 400px;
|
||||
margin: 1rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
">
|
||||
<h3 style="margin: 0 0 1rem 0; color: #333;">
|
||||
${this.getPermissionIcon(permission)} ${this.getPermissionTitle(permission)}
|
||||
</h3>
|
||||
<p style="margin: 0 0 2rem 0; color: #666; line-height: 1.5;">
|
||||
${description}
|
||||
</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
|
||||
<button class="btn-skip" style="
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
">Skip</button>
|
||||
<button class="btn-grant" style="
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
">Allow</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const skipBtn = modal.querySelector('.btn-skip');
|
||||
const grantBtn = modal.querySelector('.btn-grant');
|
||||
|
||||
const cleanup = () => document.body.removeChild(modal);
|
||||
|
||||
skipBtn.onclick = () => {
|
||||
cleanup();
|
||||
resolve('skip');
|
||||
};
|
||||
|
||||
grantBtn.onclick = () => {
|
||||
cleanup();
|
||||
resolve('grant');
|
||||
};
|
||||
|
||||
// Close on backdrop click
|
||||
modal.querySelector('.permission-dialog-backdrop').onclick = (e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
cleanup();
|
||||
resolve('cancel');
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getPermissionIcon(permission) {
|
||||
const icons = {
|
||||
camera: '📷',
|
||||
microphone: '🎤',
|
||||
geolocation: '📍',
|
||||
notifications: '🔔',
|
||||
'clipboard-read': '📋',
|
||||
'clipboard-write': '📋'
|
||||
};
|
||||
return icons[permission] || '🔐';
|
||||
},
|
||||
|
||||
getPermissionTitle(permission) {
|
||||
const titles = {
|
||||
camera: 'Camera Access',
|
||||
microphone: 'Microphone Access',
|
||||
geolocation: 'Location Access',
|
||||
notifications: 'Notifications',
|
||||
'clipboard-read': 'Clipboard Access',
|
||||
'clipboard-write': 'Clipboard Access'
|
||||
};
|
||||
return titles[permission] || `${permission} Permission`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
async cacheCurrentPermissions() {
|
||||
if (!this.support.permissions) return;
|
||||
|
||||
const commonPermissions = ['camera', 'microphone', 'geolocation', 'notifications'];
|
||||
|
||||
for (const permission of commonPermissions) {
|
||||
try {
|
||||
await this.check(permission);
|
||||
} catch (error) {
|
||||
// Ignore errors during initial caching
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPermissionDescriptor(permission) {
|
||||
// Handle different permission descriptor formats
|
||||
switch (permission) {
|
||||
case 'camera':
|
||||
return { name: 'camera' };
|
||||
case 'microphone':
|
||||
return { name: 'microphone' };
|
||||
case 'geolocation':
|
||||
return { name: 'geolocation' };
|
||||
case 'notifications':
|
||||
return { name: 'notifications' };
|
||||
case 'clipboard-read':
|
||||
return { name: 'clipboard-read' };
|
||||
case 'clipboard-write':
|
||||
return { name: 'clipboard-write' };
|
||||
case 'persistent-storage':
|
||||
return { name: 'persistent-storage' };
|
||||
case 'background-sync':
|
||||
return { name: 'background-sync' };
|
||||
case 'push':
|
||||
return { name: 'push', userVisibleOnly: true };
|
||||
default:
|
||||
return { name: permission };
|
||||
}
|
||||
}
|
||||
|
||||
async checkFallback(permission) {
|
||||
// Fallback checks for browsers without Permissions API
|
||||
const fallbacks = {
|
||||
camera: () => navigator.mediaDevices !== undefined,
|
||||
microphone: () => navigator.mediaDevices !== undefined,
|
||||
geolocation: () => 'geolocation' in navigator,
|
||||
notifications: () => 'Notification' in window,
|
||||
'clipboard-read': () => 'clipboard' in navigator,
|
||||
'clipboard-write': () => 'clipboard' in navigator
|
||||
};
|
||||
|
||||
const check = fallbacks[permission];
|
||||
const supported = check ? check() : false;
|
||||
|
||||
return {
|
||||
name: permission,
|
||||
state: supported ? 'prompt' : 'unsupported',
|
||||
timestamp: Date.now(),
|
||||
supported,
|
||||
fallback: true
|
||||
};
|
||||
}
|
||||
|
||||
async executePermissionRequest(permission, timeout) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error('Permission request timeout'));
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch (permission) {
|
||||
case 'notifications':
|
||||
if ('Notification' in window) {
|
||||
const notificationPermission = await Notification.requestPermission();
|
||||
result = { granted: notificationPermission === 'granted', state: notificationPermission };
|
||||
} else {
|
||||
result = { granted: false, state: 'unsupported' };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'camera':
|
||||
case 'microphone':
|
||||
try {
|
||||
const constraints = {};
|
||||
constraints[permission === 'camera' ? 'video' : 'audio'] = true;
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
stream.getTracks().forEach(track => track.stop()); // Stop immediately
|
||||
|
||||
result = { granted: true, state: 'granted' };
|
||||
} catch (error) {
|
||||
result = {
|
||||
granted: false,
|
||||
state: error.name === 'NotAllowedError' ? 'denied' : 'error',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'geolocation':
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
||||
timeout: 10000,
|
||||
maximumAge: 0
|
||||
});
|
||||
});
|
||||
result = { granted: true, state: 'granted' };
|
||||
} catch (error) {
|
||||
result = {
|
||||
granted: false,
|
||||
state: error.code === 1 ? 'denied' : 'error',
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Try generic permission request
|
||||
if (this.support.permissions) {
|
||||
const status = await navigator.permissions.query(this.getPermissionDescriptor(permission));
|
||||
result = { granted: status.state === 'granted', state: status.state };
|
||||
} else {
|
||||
result = { granted: false, state: 'unsupported' };
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
resolve(result);
|
||||
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setupPermissionWatcher(permission, callback) {
|
||||
if (!this.support.permissions) return;
|
||||
|
||||
try {
|
||||
const status = await navigator.permissions.query(this.getPermissionDescriptor(permission));
|
||||
status.addEventListener('change', () => {
|
||||
callback({
|
||||
permission,
|
||||
state: status.state,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.warn(`[PermissionManager] Could not set up watcher for ${permission}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
onPermissionChange(permission, newState) {
|
||||
Logger.info(`[PermissionManager] Permission changed: ${permission} → ${newState}`);
|
||||
|
||||
// Update cache
|
||||
this.permissionCache.set(permission, {
|
||||
name: permission,
|
||||
state: newState,
|
||||
timestamp: Date.now(),
|
||||
supported: true
|
||||
});
|
||||
|
||||
// Notify watchers
|
||||
this.permissionWatchers.forEach(watcher => {
|
||||
if (watcher.permission === permission && watcher.active) {
|
||||
watcher.callback({
|
||||
permission,
|
||||
state: newState,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async showPermissionRationale(permission, customMessage = null) {
|
||||
const rationales = {
|
||||
camera: 'Camera access is needed to take photos and record videos. You can enable this in your browser settings.',
|
||||
microphone: 'Microphone access is needed for audio recording and voice features. Please check your browser settings.',
|
||||
geolocation: 'Location access helps provide relevant local content. You can manage this in your browser settings.',
|
||||
notifications: 'Notifications keep you updated with important information. You can change this anytime in settings.'
|
||||
};
|
||||
|
||||
const message = customMessage || rationales[permission] || `${permission} permission was denied. Please enable it in your browser settings to use this feature.`;
|
||||
|
||||
// Simple alert for now - could be enhanced with custom UI
|
||||
if (typeof window !== 'undefined' && window.confirm) {
|
||||
return window.confirm(message + '\n\nWould you like to try again?');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
generateId(prefix = 'perm') {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive permission status report
|
||||
*/
|
||||
async getPermissionReport() {
|
||||
const report = {
|
||||
support: this.support,
|
||||
cached: Array.from(this.permissionCache.entries()).map(([name, data]) => ({
|
||||
name,
|
||||
...data
|
||||
})),
|
||||
watchers: Array.from(this.permissionWatchers.keys()),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached permissions
|
||||
*/
|
||||
clearCache() {
|
||||
this.permissionCache.clear();
|
||||
Logger.info('[PermissionManager] Permission cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all watchers and cleanup
|
||||
*/
|
||||
cleanup() {
|
||||
this.permissionWatchers.forEach(watcher => {
|
||||
watcher.active = false;
|
||||
});
|
||||
this.permissionWatchers.clear();
|
||||
this.requestQueue.clear();
|
||||
this.permissionCache.clear();
|
||||
|
||||
Logger.info('[PermissionManager] Cleanup completed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user