mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 04:31:16 -05:00
237 lines
5.9 KiB
TypeScript
237 lines
5.9 KiB
TypeScript
/**
|
|
* Lock Auto-Release Mechanism
|
|
*
|
|
* Automatically releases submission locks when operations fail, timeout,
|
|
* or are abandoned by moderators. Prevents deadlocks and improves queue flow.
|
|
*
|
|
* Part of Sacred Pipeline Phase 4: Transaction Resilience
|
|
*/
|
|
|
|
import { supabase } from '@/lib/supabaseClient';
|
|
import { logger } from '@/lib/logger';
|
|
import { isTimeoutError } from '@/lib/timeoutDetection';
|
|
import { toast } from '@/hooks/use-toast';
|
|
|
|
export interface LockReleaseOptions {
|
|
submissionId: string;
|
|
moderatorId: string;
|
|
reason: 'timeout' | 'error' | 'abandoned' | 'manual';
|
|
error?: unknown;
|
|
silent?: boolean; // Don't show toast notification
|
|
}
|
|
|
|
/**
|
|
* Release a lock on a submission
|
|
*/
|
|
export async function releaseLock(options: LockReleaseOptions): Promise<boolean> {
|
|
const { submissionId, moderatorId, reason, error, silent = false } = options;
|
|
|
|
try {
|
|
// Call Supabase RPC to release lock
|
|
const { error: releaseError } = await supabase.rpc('release_submission_lock', {
|
|
submission_id: submissionId,
|
|
moderator_id: moderatorId,
|
|
});
|
|
|
|
if (releaseError) {
|
|
logger.error('Failed to release lock', {
|
|
submissionId,
|
|
moderatorId,
|
|
reason,
|
|
error: releaseError,
|
|
});
|
|
|
|
if (!silent) {
|
|
toast({
|
|
title: 'Lock Release Failed',
|
|
description: 'Failed to release submission lock. It will expire automatically.',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
logger.info('Lock released', {
|
|
submissionId,
|
|
moderatorId,
|
|
reason,
|
|
hasError: !!error,
|
|
});
|
|
|
|
if (!silent) {
|
|
const message = getLockReleaseMessage(reason);
|
|
toast({
|
|
title: 'Lock Released',
|
|
description: message,
|
|
});
|
|
}
|
|
|
|
return true;
|
|
} catch (err) {
|
|
logger.error('Exception while releasing lock', {
|
|
submissionId,
|
|
moderatorId,
|
|
reason,
|
|
error: err,
|
|
});
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Auto-release lock when an operation fails
|
|
*
|
|
* @param submissionId - Submission ID
|
|
* @param moderatorId - Moderator ID
|
|
* @param error - Error that triggered the release
|
|
*/
|
|
export async function autoReleaseLockOnError(
|
|
submissionId: string,
|
|
moderatorId: string,
|
|
error: unknown
|
|
): Promise<void> {
|
|
const isTimeout = isTimeoutError(error);
|
|
|
|
logger.warn('Auto-releasing lock due to error', {
|
|
submissionId,
|
|
moderatorId,
|
|
isTimeout,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
|
|
await releaseLock({
|
|
submissionId,
|
|
moderatorId,
|
|
reason: isTimeout ? 'timeout' : 'error',
|
|
error,
|
|
silent: false, // Show notification for transparency
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Auto-release lock when moderator abandons review
|
|
* Triggered by navigation away, tab close, or inactivity
|
|
*/
|
|
export async function autoReleaseLockOnAbandon(
|
|
submissionId: string,
|
|
moderatorId: string
|
|
): Promise<void> {
|
|
logger.info('Auto-releasing lock due to abandonment', {
|
|
submissionId,
|
|
moderatorId,
|
|
});
|
|
|
|
await releaseLock({
|
|
submissionId,
|
|
moderatorId,
|
|
reason: 'abandoned',
|
|
silent: true, // Silent for better UX
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup auto-release on page unload (user navigates away or closes tab)
|
|
*/
|
|
export function setupAutoReleaseOnUnload(
|
|
submissionId: string,
|
|
moderatorId: string
|
|
): () => void {
|
|
const handleUnload = () => {
|
|
// Use sendBeacon for reliable unload requests
|
|
const payload = JSON.stringify({
|
|
submission_id: submissionId,
|
|
moderator_id: moderatorId,
|
|
});
|
|
|
|
// Try to call RPC via sendBeacon (more reliable on unload)
|
|
const url = `${import.meta.env.VITE_SUPABASE_URL}/rest/v1/rpc/release_submission_lock`;
|
|
const blob = new Blob([payload], { type: 'application/json' });
|
|
|
|
navigator.sendBeacon(url, blob);
|
|
|
|
logger.info('Scheduled lock release on unload', {
|
|
submissionId,
|
|
moderatorId,
|
|
});
|
|
};
|
|
|
|
// Add listeners
|
|
window.addEventListener('beforeunload', handleUnload);
|
|
window.addEventListener('pagehide', handleUnload);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
window.removeEventListener('beforeunload', handleUnload);
|
|
window.removeEventListener('pagehide', handleUnload);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Monitor inactivity and auto-release after timeout
|
|
*
|
|
* @param submissionId - Submission ID
|
|
* @param moderatorId - Moderator ID
|
|
* @param inactivityMinutes - Minutes of inactivity before release (default: 10)
|
|
* @returns Cleanup function
|
|
*/
|
|
export function setupInactivityAutoRelease(
|
|
submissionId: string,
|
|
moderatorId: string,
|
|
inactivityMinutes: number = 10
|
|
): () => void {
|
|
let inactivityTimer: NodeJS.Timeout | null = null;
|
|
|
|
const resetTimer = () => {
|
|
if (inactivityTimer) {
|
|
clearTimeout(inactivityTimer);
|
|
}
|
|
|
|
inactivityTimer = setTimeout(() => {
|
|
logger.warn('Inactivity timeout - auto-releasing lock', {
|
|
submissionId,
|
|
moderatorId,
|
|
inactivityMinutes,
|
|
});
|
|
|
|
autoReleaseLockOnAbandon(submissionId, moderatorId);
|
|
}, inactivityMinutes * 60 * 1000);
|
|
};
|
|
|
|
// Track user activity
|
|
const activityEvents = ['mousedown', 'keydown', 'scroll', 'touchstart'];
|
|
activityEvents.forEach(event => {
|
|
window.addEventListener(event, resetTimer, { passive: true });
|
|
});
|
|
|
|
// Start timer
|
|
resetTimer();
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
if (inactivityTimer) {
|
|
clearTimeout(inactivityTimer);
|
|
}
|
|
activityEvents.forEach(event => {
|
|
window.removeEventListener(event, resetTimer);
|
|
});
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get user-friendly lock release message
|
|
*/
|
|
function getLockReleaseMessage(reason: LockReleaseOptions['reason']): string {
|
|
switch (reason) {
|
|
case 'timeout':
|
|
return 'Lock released due to timeout. The submission is available for other moderators.';
|
|
case 'error':
|
|
return 'Lock released due to an error. You can reclaim it to continue reviewing.';
|
|
case 'abandoned':
|
|
return 'Lock released. The submission is back in the queue.';
|
|
case 'manual':
|
|
return 'Lock released successfully.';
|
|
}
|
|
}
|