human fight sequence fix
This commit is contained in:
@@ -65,6 +65,7 @@ export function useFightPolling(fightId: Ref<string>) {
|
|||||||
function startPolling(callbacks?: {
|
function startPolling(callbacks?: {
|
||||||
onFinished?: () => void
|
onFinished?: () => void
|
||||||
onTimeout?: () => void
|
onTimeout?: () => void
|
||||||
|
keepLive?: boolean // Don't set isLive=false (human fights manage lifecycle via SSE)
|
||||||
}) {
|
}) {
|
||||||
pollCount = 0
|
pollCount = 0
|
||||||
pollHandle = setInterval(async () => {
|
pollHandle = setInterval(async () => {
|
||||||
@@ -72,7 +73,7 @@ export function useFightPolling(fightId: Ref<string>) {
|
|||||||
const s = await loadFight()
|
const s = await loadFight()
|
||||||
if (s === 'finished') {
|
if (s === 'finished') {
|
||||||
if (!eventSource) {
|
if (!eventSource) {
|
||||||
isLive.value = false
|
if (!callbacks?.keepLive) isLive.value = false
|
||||||
disconnectSSE()
|
disconnectSSE()
|
||||||
stopPolling()
|
stopPolling()
|
||||||
callbacks?.onFinished?.()
|
callbacks?.onFinished?.()
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { ref, type Ref } from 'vue'
|
|||||||
|
|
||||||
// Buffer to ensure client timer finishes BEFORE server timer
|
// Buffer to ensure client timer finishes BEFORE server timer
|
||||||
// (accounts for SSE delivery delay + network latency)
|
// (accounts for SSE delivery delay + network latency)
|
||||||
const CLIENT_TIMER_BUFFER_MS = 1500
|
const CLIENT_TIMER_BUFFER_MS = 1000
|
||||||
|
|
||||||
|
// Minimum time (ms) to display a challenge — even if server has less remaining
|
||||||
|
const MIN_DISPLAY_MS = 5000
|
||||||
|
|
||||||
export function useHumanChallenge(
|
export function useHumanChallenge(
|
||||||
fightId: Ref<string>,
|
fightId: Ref<string>,
|
||||||
@@ -28,19 +31,26 @@ export function useHumanChallenge(
|
|||||||
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
||||||
let timerHandle: ReturnType<typeof setInterval> | null = null
|
let timerHandle: ReturnType<typeof setInterval> | null = null
|
||||||
let cooldownHandle: ReturnType<typeof setInterval> | null = null
|
let cooldownHandle: ReturnType<typeof setInterval> | null = null
|
||||||
|
// Track which round was last applied to prevent double-apply from SSE+polling
|
||||||
|
let lastAppliedRound = 0
|
||||||
|
|
||||||
function applyChallenge(data: any, receivedAt?: number) {
|
function applyChallenge(data: any, receivedAt?: number) {
|
||||||
|
const roundNum = data.roundNumber || data.round
|
||||||
|
// Deduplicate: don't re-apply the same round
|
||||||
|
if (roundNum && roundNum === lastAppliedRound && humanChallenge.value) return
|
||||||
|
lastAppliedRound = roundNum || 0
|
||||||
|
|
||||||
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
const elapsed = receivedAt ? Date.now() - receivedAt : 0
|
||||||
// Use server-computed remainingMs when available (from polling), otherwise derive from timeoutMs
|
// Use server-computed remainingMs when available (from polling), otherwise derive from timeoutMs
|
||||||
const hasChoices = data.choices && data.choices.length > 0
|
const rawTimeout = data.remainingMs || (data.timeoutMs || 8000)
|
||||||
const rawTimeout = data.remainingMs || (hasChoices ? Math.min(data.timeoutMs || 8000, 10000) : (data.timeoutMs || 8000))
|
|
||||||
// Subtract buffer so client always submits before server times out
|
// Subtract buffer so client always submits before server times out
|
||||||
const remaining = Math.max(1000, rawTimeout - elapsed - CLIENT_TIMER_BUFFER_MS)
|
// But guarantee at least MIN_DISPLAY_MS so the user always has time to read + answer
|
||||||
|
const remaining = Math.max(MIN_DISPLAY_MS, rawTimeout - elapsed - CLIENT_TIMER_BUFFER_MS)
|
||||||
humanChallenge.value = {
|
humanChallenge.value = {
|
||||||
type: data.type,
|
type: data.type,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
prompt: data.prompt,
|
prompt: data.prompt,
|
||||||
roundNumber: data.roundNumber || data.round,
|
roundNumber: roundNum,
|
||||||
remainingMs: remaining,
|
remainingMs: remaining,
|
||||||
scoring: data.scoring,
|
scoring: data.scoring,
|
||||||
}
|
}
|
||||||
@@ -65,29 +75,36 @@ export function useHumanChallenge(
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitHumanAnswer() {
|
/** Send an answer to the server (shared by text input + choice buttons) */
|
||||||
if (!myBotId.value || !humanChallenge.value || humanSubmitted.value) return
|
async function sendAnswer(answer: string) {
|
||||||
if (!humanAnswer.value.trim()) return
|
if (!myBotId.value || !humanChallenge.value) return
|
||||||
|
|
||||||
humanSubmitted.value = true
|
|
||||||
if (timerHandle) clearInterval(timerHandle)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/fights/${fightId.value}/respond/${myBotId.value}`, {
|
await fetch(`/api/fights/${fightId.value}/respond/${myBotId.value}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ answer: humanAnswer.value.trim() }),
|
body: JSON.stringify({ answer }),
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[HumanChallenge] submitHumanAnswer failed:', err)
|
console.warn('[HumanChallenge] sendAnswer failed:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Submit from text input */
|
||||||
|
async function submitHumanAnswer() {
|
||||||
|
if (!myBotId.value || !humanChallenge.value || humanSubmitted.value) return
|
||||||
|
if (!humanAnswer.value.trim()) return
|
||||||
|
humanSubmitted.value = true
|
||||||
|
if (timerHandle) clearInterval(timerHandle)
|
||||||
|
await sendAnswer(humanAnswer.value.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Submit from multiple-choice button tap */
|
||||||
function submitChoice(choice: string) {
|
function submitChoice(choice: string) {
|
||||||
if (humanSubmitted.value) return // Prevent double-tap
|
if (humanSubmitted.value) return // Prevent double-tap
|
||||||
humanAnswer.value = choice
|
humanAnswer.value = choice
|
||||||
humanSubmitted.value = true // Lock immediately before async
|
humanSubmitted.value = true
|
||||||
void submitHumanAnswer()
|
if (timerHandle) clearInterval(timerHandle)
|
||||||
|
void sendAnswer(choice)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitTimeout() {
|
async function submitTimeout() {
|
||||||
@@ -110,12 +127,11 @@ export function useHumanChallenge(
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`)
|
const res = await fetch(`/api/fights/${fightId.value}/challenge/${myBotId.value}`)
|
||||||
if (res.status === 429) {
|
if (res.status === 429) {
|
||||||
// Back off on rate limit — skip next few polls
|
|
||||||
pollBackoff = Math.min(pollBackoff + 2, 8)
|
pollBackoff = Math.min(pollBackoff + 2, 8)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
pollBackoff = Math.max(0, pollBackoff - 1) // Recover gradually
|
pollBackoff = Math.max(0, pollBackoff - 1)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (data.pending) {
|
if (data.pending) {
|
||||||
@@ -129,7 +145,6 @@ export function useHumanChallenge(
|
|||||||
if (!humanChallenge.value || humanChallenge.value.roundNumber !== data.roundNumber) {
|
if (!humanChallenge.value || humanChallenge.value.roundNumber !== data.roundNumber) {
|
||||||
applyChallenge(data)
|
applyChallenge(data)
|
||||||
} else if (data.choices?.length && humanChoices.value.length === 0) {
|
} else if (data.choices?.length && humanChoices.value.length === 0) {
|
||||||
// SSE fallback had no choices, polling found them — update
|
|
||||||
humanChoices.value = data.choices
|
humanChoices.value = data.choices
|
||||||
}
|
}
|
||||||
} else if (data.fightStatus === 'finished') {
|
} else if (data.fightStatus === 'finished') {
|
||||||
@@ -160,7 +175,6 @@ export function useHumanChallenge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startCooldown(seconds: number) {
|
function startCooldown(seconds: number) {
|
||||||
// Clear any existing cooldown interval to prevent double-decrement
|
|
||||||
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
||||||
roundCooldown.value = seconds
|
roundCooldown.value = seconds
|
||||||
cooldownHandle = setInterval(() => {
|
cooldownHandle = setInterval(() => {
|
||||||
@@ -178,6 +192,7 @@ export function useHumanChallenge(
|
|||||||
function clearChallenge() {
|
function clearChallenge() {
|
||||||
humanChallenge.value = null
|
humanChallenge.value = null
|
||||||
humanSubmitted.value = false
|
humanSubmitted.value = false
|
||||||
|
pendingChallengeData.value = null
|
||||||
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
|
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +210,13 @@ export function useHumanChallenge(
|
|||||||
pendingChallengeData.value = null
|
pendingChallengeData.value = null
|
||||||
entrancePlaying.value = false
|
entrancePlaying.value = false
|
||||||
animatingRound.value = false
|
animatingRound.value = false
|
||||||
|
lastAppliedRound = 0
|
||||||
|
if (timerHandle) { clearInterval(timerHandle); timerHandle = null }
|
||||||
|
if (cooldownHandle) { clearInterval(cooldownHandle); cooldownHandle = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark entrance as playing — queues any challenges that arrive during animation */
|
|
||||||
function setEntrancePlaying(playing: boolean) {
|
function setEntrancePlaying(playing: boolean) {
|
||||||
entrancePlaying.value = playing
|
entrancePlaying.value = playing
|
||||||
// When entrance finishes, flush any queued challenge
|
|
||||||
if (!playing && pendingChallengeData.value) {
|
if (!playing && pendingChallengeData.value) {
|
||||||
applyChallenge(pendingChallengeData.value.data, pendingChallengeData.value.receivedAt)
|
applyChallenge(pendingChallengeData.value.data, pendingChallengeData.value.receivedAt)
|
||||||
pendingChallengeData.value = null
|
pendingChallengeData.value = null
|
||||||
@@ -208,7 +224,6 @@ export function useHumanChallenge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSSEChallenge(sseData: any) {
|
function handleSSEChallenge(sseData: any) {
|
||||||
// Queue challenges during entrance animation, round animation, or cooldown
|
|
||||||
if (entrancePlaying.value || animatingRound.value || roundCooldown.value > 0) {
|
if (entrancePlaying.value || animatingRound.value || roundCooldown.value > 0) {
|
||||||
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
pendingChallengeData.value = { data: sseData, receivedAt: Date.now() }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const challenge = useHumanChallenge(fightId, myBotId)
|
|||||||
const {
|
const {
|
||||||
humanChallenge, humanAnswer, humanTimer, humanSubmitted, humanChoices,
|
humanChallenge, humanAnswer, humanTimer, humanSubmitted, humanChoices,
|
||||||
roundCooldown, pendingChallengeData,
|
roundCooldown, pendingChallengeData,
|
||||||
applyChallenge, submitChoice, startHumanPolling, stopHumanPolling,
|
applyChallenge, submitHumanAnswer, submitChoice, startHumanPolling, stopHumanPolling,
|
||||||
startCooldown, clearChallenge, stopCooldown, resetState: resetChallengeState, handleSSEChallenge,
|
startCooldown, clearChallenge, stopCooldown, resetState: resetChallengeState, handleSSEChallenge,
|
||||||
setEntrancePlaying, animatingRound,
|
setEntrancePlaying, animatingRound,
|
||||||
} = challenge
|
} = challenge
|
||||||
@@ -452,7 +452,7 @@ watch(() => route.params.fightId, async (newId) => {
|
|||||||
isLive.value = true
|
isLive.value = true
|
||||||
}
|
}
|
||||||
if (isLive.value) {
|
if (isLive.value) {
|
||||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling(), keepLive: isHumanFight.value })
|
||||||
wireSSE()
|
wireSSE()
|
||||||
if (isHumanFight.value) {
|
if (isHumanFight.value) {
|
||||||
startHumanPolling()
|
startHumanPolling()
|
||||||
@@ -523,7 +523,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isLive.value) {
|
if (isLive.value) {
|
||||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling(), keepLive: isHumanFight.value })
|
||||||
if (isHumanFight.value) {
|
if (isHumanFight.value) {
|
||||||
wireSSE()
|
wireSSE()
|
||||||
// Init scene first, then start challenge polling — ensures entrance
|
// Init scene first, then start challenge polling — ensures entrance
|
||||||
@@ -567,7 +567,8 @@ async function fightAgain(botId: string) {
|
|||||||
liveHpA.value = 100
|
liveHpA.value = 100
|
||||||
liveHpB.value = 100
|
liveHpB.value = 100
|
||||||
liveCurrentRound.value = 0
|
liveCurrentRound.value = 0
|
||||||
// Stop ALL old polling/connections before starting new fight
|
// Stop ALL old audio/polling/connections before starting new fight
|
||||||
|
stopAllAudio()
|
||||||
stopHumanPolling()
|
stopHumanPolling()
|
||||||
stopPolling()
|
stopPolling()
|
||||||
disconnectSSE()
|
disconnectSSE()
|
||||||
@@ -582,7 +583,7 @@ async function fightAgain(botId: string) {
|
|||||||
liveRounds.value = 0
|
liveRounds.value = 0
|
||||||
fightError.value = ''
|
fightError.value = ''
|
||||||
window.history.replaceState({}, '', `/arena/${data.fightId}`)
|
window.history.replaceState({}, '', `/arena/${data.fightId}`)
|
||||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling(), keepLive: isHumanFight.value })
|
||||||
if (isHumanFight.value) {
|
if (isHumanFight.value) {
|
||||||
wireSSE()
|
wireSSE()
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
@@ -621,6 +622,7 @@ async function matchmake(botId: string) {
|
|||||||
liveHpA.value = 100
|
liveHpA.value = 100
|
||||||
liveHpB.value = 100
|
liveHpB.value = 100
|
||||||
liveCurrentRound.value = 0
|
liveCurrentRound.value = 0
|
||||||
|
stopAllAudio()
|
||||||
stopHumanPolling()
|
stopHumanPolling()
|
||||||
stopPolling()
|
stopPolling()
|
||||||
disconnectSSE()
|
disconnectSSE()
|
||||||
@@ -634,7 +636,7 @@ async function matchmake(botId: string) {
|
|||||||
liveRounds.value = 0
|
liveRounds.value = 0
|
||||||
fightError.value = ''
|
fightError.value = ''
|
||||||
window.history.replaceState({}, '', `/arena/${data.fightId}`)
|
window.history.replaceState({}, '', `/arena/${data.fightId}`)
|
||||||
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling() })
|
startPolling({ onFinished: () => stopHumanPolling(), onTimeout: () => stopHumanPolling(), keepLive: isHumanFight.value })
|
||||||
} else {
|
} else {
|
||||||
fightError.value = `Matchmaking failed (${res.status})`
|
fightError.value = `Matchmaking failed (${res.status})`
|
||||||
}
|
}
|
||||||
@@ -784,7 +786,7 @@ function stopAutoBattle() {
|
|||||||
{{ humanChallenge.prompt }}
|
{{ humanChallenge.prompt }}
|
||||||
</p>
|
</p>
|
||||||
<!-- Multiple choice buttons -->
|
<!-- Multiple choice buttons -->
|
||||||
<div class="flex flex-col gap-2">
|
<div v-if="humanChoices.length > 0" class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
v-for="(choice, i) in humanChoices"
|
v-for="(choice, i) in humanChoices"
|
||||||
:key="i"
|
:key="i"
|
||||||
@@ -800,6 +802,27 @@ function stopAutoBattle() {
|
|||||||
{{ choice }}
|
{{ choice }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Text input for open-ended / creative questions -->
|
||||||
|
<form v-else @submit.prevent="submitHumanAnswer" class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="humanAnswer"
|
||||||
|
type="text"
|
||||||
|
class="flex-1 px-3 py-2 bg-black/50 border-2 border-neon-cyan/30 rounded-lg
|
||||||
|
font-mono text-sm text-text-primary placeholder-text-muted/50
|
||||||
|
focus:outline-none focus:border-neon-cyan/60"
|
||||||
|
placeholder="Type your answer..."
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="!humanAnswer.trim()"
|
||||||
|
class="px-4 py-2 bg-neon-cyan/10 border-2 border-neon-cyan/50 text-neon-cyan
|
||||||
|
font-display font-bold text-xs tracking-wider rounded-lg
|
||||||
|
hover:bg-neon-cyan/20 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
|
||||||
|
>
|
||||||
|
SEND
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="humanSubmitted" class="text-center py-3">
|
<div v-else-if="humanSubmitted" class="text-center py-3">
|
||||||
<p class="font-display text-text-muted text-sm tracking-wider animate-pulse">SCORING ROUND...</p>
|
<p class="font-display text-text-muted text-sm tracking-wider animate-pulse">SCORING ROUND...</p>
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ const showTrashTalk = ref(false)
|
|||||||
|
|
||||||
// Buffer to ensure client timer finishes BEFORE server timer
|
// Buffer to ensure client timer finishes BEFORE server timer
|
||||||
// (accounts for polling delivery delay + network latency)
|
// (accounts for polling delivery delay + network latency)
|
||||||
const CLIENT_TIMER_BUFFER_MS = 1500
|
const CLIENT_TIMER_BUFFER_MS = 1000
|
||||||
|
// Minimum display time so user always has time to read + answer
|
||||||
|
const MIN_DISPLAY_MS = 5000
|
||||||
|
|
||||||
// Feedback state
|
// Feedback state
|
||||||
const feedback = ref<'correct' | 'wrong' | null>(null)
|
const feedback = ref<'correct' | 'wrong' | null>(null)
|
||||||
@@ -200,10 +202,10 @@ async function pollForChallenge() {
|
|||||||
showTrashTalk.value = false
|
showTrashTalk.value = false
|
||||||
feedback.value = null
|
feedback.value = null
|
||||||
phase.value = 'challenge'
|
phase.value = 'challenge'
|
||||||
// Cap timer for multiple choice — just tapping a button, not typing
|
|
||||||
// Subtract buffer so client always submits before server times out
|
// Subtract buffer so client always submits before server times out
|
||||||
const rawMs = (data.choices?.length > 0) ? Math.min(data.remainingMs, 10000) : data.remainingMs
|
// Guarantee at least MIN_DISPLAY_MS so user always has time to read + answer
|
||||||
const timerMs = Math.max(1000, rawMs - CLIENT_TIMER_BUFFER_MS)
|
const rawMs = data.remainingMs
|
||||||
|
const timerMs = Math.max(MIN_DISPLAY_MS, rawMs - CLIENT_TIMER_BUFFER_MS)
|
||||||
startTimer(timerMs)
|
startTimer(timerMs)
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (!hasChoices.value) answerInput.value?.focus()
|
if (!hasChoices.value) answerInput.value?.focus()
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export function clearAllPending(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_HUMAN_TIMEOUT_MS = 8_000
|
const DEFAULT_HUMAN_TIMEOUT_MS = 8_000
|
||||||
|
// Extra time for humans: accounts for network latency, SSE/polling delivery delay, reading time
|
||||||
|
const HUMAN_EXTRA_MS = 5_000
|
||||||
|
|
||||||
export function isHumanPlayer(webhookUrl: string): boolean {
|
export function isHumanPlayer(webhookUrl: string): boolean {
|
||||||
return webhookUrl === 'http://human.local/'
|
return webhookUrl === 'http://human.local/'
|
||||||
@@ -115,7 +117,7 @@ export function waitForHumanResponse(
|
|||||||
const choices = generateChoices(challenge)
|
const choices = generateChoices(challenge)
|
||||||
const promise = new Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }>((resolve) => {
|
const promise = new Promise<{ answer: string | null; trashTalk?: string; timedOut: boolean }>((resolve) => {
|
||||||
const key = `${fightId}:${botId}`
|
const key = `${fightId}:${botId}`
|
||||||
const timeoutMs = challenge.timeout_ms || DEFAULT_HUMAN_TIMEOUT_MS
|
const timeoutMs = (challenge.timeout_ms || DEFAULT_HUMAN_TIMEOUT_MS) + HUMAN_EXTRA_MS
|
||||||
|
|
||||||
const timeoutHandle = setTimeout(() => {
|
const timeoutHandle = setTimeout(() => {
|
||||||
pending.delete(key)
|
pending.delete(key)
|
||||||
@@ -174,7 +176,7 @@ export function getPendingChallenge(
|
|||||||
const entry = pending.get(key)
|
const entry = pending.get(key)
|
||||||
if (!entry) return null
|
if (!entry) return null
|
||||||
|
|
||||||
const timeoutMs = entry.challenge.timeout_ms || DEFAULT_HUMAN_TIMEOUT_MS
|
const timeoutMs = (entry.challenge.timeout_ms || DEFAULT_HUMAN_TIMEOUT_MS) + HUMAN_EXTRA_MS
|
||||||
const elapsed = Date.now() - entry.createdAt
|
const elapsed = Date.now() - entry.createdAt
|
||||||
const remaining = Math.max(0, timeoutMs - elapsed)
|
const remaining = Math.max(0, timeoutMs - elapsed)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user