Fix race condition in NameEditor

This commit is contained in:
2025-04-16 18:14:39 -04:00
parent 1c56e07b4b
commit 9ff8af7a25

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import {computed, ref} from "vue"; import {ref, onMounted, onUnmounted} from "vue";
import {v7} from "uuid"; import {v7} from "uuid";
import {useClient} from "@/plugins/api.js"; import {useClient} from "@/plugins/api.js";
@@ -18,44 +18,93 @@ const emits = defineEmits([
]); ]);
const name = ref(props.name); const name = ref(props.name);
const isReserved = computed(() => reservationState.value === 'reserved');
const isOperationPending = ref(false); const isOperationPending = ref(false);
const reservationState = ref(null); const reservationState = ref(null);
const reservationId = ref(v7());
// Use the reservationId from props if provided, otherwise generate a new one
const reservationId = ref(props.creatorNameReservationId || v7());
// Ensure we emit the reservationId on mount if we generated a new one
onMounted(() => {
if (!props.creatorNameReservationId) {
emits('update:creatorNameReservationId', reservationId.value);
}
});
// Request handling
let currentController = null;
let timeout = null; let timeout = null;
const handleInput = () => { let lastProcessedName = '';
clearTimeout(timeout);
timeout = setTimeout(() => const cancelCurrentRequest = () => {
checkNameAvailability(), if (currentController) {
200); currentController.abort();
currentController = null;
}
}; };
const client = useClient() const handleInput = () => {
const checkNameAvailability = async () => { clearTimeout(timeout);
if (!name.value || name.value.trim() === "") { timeout = setTimeout(() => {
const currentName = name.value;
if (currentName === lastProcessedName) {
return; // Skip if we've already processed this exact name
}
checkNameAvailability(currentName);
}, 200);
};
const client = useClient();
const checkNameAvailability = async (nameToCheck) => {
if (!nameToCheck || nameToCheck.trim() === "") {
reservationState.value = null; reservationState.value = null;
lastProcessedName = nameToCheck;
return; return;
} }
// Cancel any ongoing request
cancelCurrentRequest();
try { try {
isOperationPending.value = true; isOperationPending.value = true;
reservationState.value = "loading"; reservationState.value = "loading";
// Create a new request with cancellation token
const controller = new AbortController();
currentController = controller;
await client.post( await client.post(
`/api/creators/@${encodeURIComponent(name.value)}/reserve`, `/api/creators/@${encodeURIComponent(nameToCheck)}/reserve`,
{reservationId: reservationId.value} { reservationId: reservationId.value },
{ signal: controller.signal }
); );
// Only process the response if this is still the current request
if (currentController === controller) {
reservationState.value = "reserved"; reservationState.value = "reserved";
lastProcessedName = nameToCheck;
emits('update:name', nameToCheck);
}
} catch (error) { } catch (error) {
reservationState.value = "unavailable"; // Handle API failure case // Only process the error if this is still the current request and it's not an abort error
if (currentController && error.name !== 'AbortError') {
reservationState.value = "unavailable";
lastProcessedName = nameToCheck;
}
} finally { } finally {
emits('update:name', name.value); if (currentController) {
emits('update:creatorNameReservationId', reservationId.value);
isOperationPending.value = false; isOperationPending.value = false;
} }
}
}; };
// Cleanup on component unmount
onUnmounted(() => {
cancelCurrentRequest();
clearTimeout(timeout);
});
</script> </script>
<template> <template>