Compare commits

...
Author SHA1 Message Date
Jeppe B 18ab007f87 Debounce self-serve wash data fetches 2026-06-02 13:41:55 +02:00
3 changed files with 197 additions and 14 deletions
+42 -3
View File
@@ -297,6 +297,30 @@ export function useSelfServeLogic() {
const summaryVisibleQuestionIds = ref([]);
const summaryQuestionOrder = ref({});
const latestFetchRequestId = ref(0);
const latestSuccessfulFetchKey = ref(null);
const inFlightFetchKey = ref(null);
const createFetchKey = (departmentId, vehicleTypeId, laneId, reg) => {
const normalizedDepartmentId = parseInt(departmentId);
const normalizedLaneId = parseInt(laneId);
const normalizedReg = String(reg || "").trim().toUpperCase();
const normalizedVehicleTypeId = parseInt(vehicleTypeId);
const vehicleTypeKey = !Number.isNaN(normalizedVehicleTypeId) && normalizedVehicleTypeId > 0
? normalizedVehicleTypeId
: "";
if (
Number.isNaN(normalizedDepartmentId)
|| normalizedDepartmentId <= 0
|| Number.isNaN(normalizedLaneId)
|| normalizedLaneId <= 0
|| normalizedReg.length < 2
) {
return null;
}
return [normalizedDepartmentId, normalizedLaneId, normalizedReg, vehicleTypeKey].join("|");
};
const beginFetchRequest = () => {
latestFetchRequestId.value += 1;
@@ -575,10 +599,17 @@ export function useSelfServeLogic() {
}
};
const fetchSelfServeData = async (_departmentId, _vehicleTypeId, laneId = null, reg = null) => {
const fetchSelfServeData = async (_departmentId, _vehicleTypeId, laneId = null, reg = null, options = {}) => {
const fetchKey = createFetchKey(_departmentId, _vehicleTypeId, laneId, reg);
if (!options.force && fetchKey && (fetchKey === latestSuccessfulFetchKey.value || fetchKey === inFlightFetchKey.value)) {
return null;
}
const requestId = beginFetchRequest();
if (!laneId || !reg || reg.trim().length < 2) {
latestSuccessfulFetchKey.value = null;
inFlightFetchKey.value = null;
preview.value = null;
summary.value = null;
session.value = null;
@@ -606,6 +637,7 @@ export function useSelfServeLogic() {
loading.value = true;
requestError.value = null;
inFlightFetchKey.value = fetchKey;
try {
const normalizedReg = reg.trim().toUpperCase();
const normalizedVehicleTypeId = parseInt(_vehicleTypeId);
@@ -687,6 +719,10 @@ export function useSelfServeLogic() {
setSummaryVisibleQuestions(questions.value);
}
if (isFetchRequestActive(requestId)) {
latestSuccessfulFetchKey.value = fetchKey;
}
return previewData;
} catch (error) {
console.error("Error fetching self-serve preview:", error);
@@ -695,6 +731,9 @@ export function useSelfServeLogic() {
}
return null;
} finally {
if (inFlightFetchKey.value === fetchKey) {
inFlightFetchKey.value = null;
}
if (isFetchRequestActive(requestId)) {
loading.value = false;
}
@@ -749,7 +788,7 @@ export function useSelfServeLogic() {
updateResolvedVehicleTypeId(responseSummary, responseSummary?.session);
}
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg);
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg, { force: true });
answers.value = {
...answers.value,
[parseInt(questionId)]: value,
@@ -814,7 +853,7 @@ export function useSelfServeLogic() {
? normalizedRefreshVehicleTypeId
: null;
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg);
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg, { force: true });
return { deletedCount: conditionIdsToDelete.length };
} catch (error) {
@@ -60,6 +60,7 @@ const RECENT_COMPLETED_WASH_KEY = "mywash_recent_completed_v1";
const RECENT_COMPLETED_WASH_SUPPRESSION_MS = 10 * 60 * 1000;
const ACTIVE_WASH_REFRESH_MS = 5 * 1000;
const WASH_START_SERVER_SYNC_GRACE_MS = 10 * 1000;
const SELF_SERVE_FETCH_DEBOUNCE_MS = 300;
const normalizePositiveInteger = (value: any) => {
const parsed = parseInt(String(value ?? ""), 10);
@@ -637,24 +638,129 @@ const fetchVehicleTypes = async () => {
}
};
const fetchSelfServeData = async () => {
if (isRestoring.value || !nearestDepartment.value) {
return;
type SelfServeFetchRequest = {
key: string;
departmentId: number;
laneId: number;
registration: string;
vehicleTypeId: number | null;
};
let selfServeFetchDebounceTimer: ReturnType<typeof window.setTimeout> | null = null;
let selfServeFetchResolvers: Array<(value: any) => void> = [];
let latestSuccessfulSelfServeFetchKey: string | null = null;
let inFlightSelfServeFetchKey: string | null = null;
let selfServeFetchSequence = 0;
let isSelfServeFetchUnmounted = false;
const resolvePendingSelfServeFetches = (value: any = null) => {
const resolvers = selfServeFetchResolvers;
selfServeFetchResolvers = [];
resolvers.forEach((resolve) => resolve(value));
};
const clearScheduledSelfServeFetch = (resolveValue: any = null) => {
if (selfServeFetchDebounceTimer) {
window.clearTimeout(selfServeFetchDebounceTimer);
selfServeFetchDebounceTimer = null;
}
const normalizedReg = normalizeLicensePlate(licensePlateInput.value);
if (normalizedReg.length < 2) {
return;
if (selfServeFetchResolvers.length > 0) {
resolvePendingSelfServeFetches(resolveValue);
}
};
const createSelfServeFetchRequest = (): SelfServeFetchRequest | null => {
if (isRestoring.value || !nearestDepartment.value) {
return null;
}
const departmentId = normalizePositiveInteger(nearestDepartment.value.id);
if (!departmentId) {
return null;
}
const registration = normalizeLicensePlate(licensePlateInput.value);
if (registration.length < 2) {
return null;
}
const laneId = ensureEffectiveLaneSelection();
if (!laneId) {
return;
return null;
}
const departmentId = nearestDepartment.value.id;
const vehicleTypeId = normalizePositiveInteger(vehicleTypeSelect.value);
const key = [departmentId, laneId, registration, vehicleTypeId ?? ""].join("|");
await fetchSelfServeDataInternal(departmentId, vehicleTypeSelect.value || null, laneId, normalizedReg);
return {
key,
departmentId,
laneId,
registration,
vehicleTypeId,
};
};
const executeSelfServeFetch = async (request: SelfServeFetchRequest | null = createSelfServeFetchRequest()) => {
if (!request || isSelfServeFetchUnmounted) {
return null;
}
if (request.key === latestSuccessfulSelfServeFetchKey || request.key === inFlightSelfServeFetchKey) {
return null;
}
const requestSequence = ++selfServeFetchSequence;
inFlightSelfServeFetchKey = request.key;
try {
const result = await fetchSelfServeDataInternal(
request.departmentId,
request.vehicleTypeId,
request.laneId,
request.registration
);
if (!isSelfServeFetchUnmounted && requestSequence === selfServeFetchSequence) {
latestSuccessfulSelfServeFetchKey = request.key;
}
return result;
} finally {
if (inFlightSelfServeFetchKey === request.key) {
inFlightSelfServeFetchKey = null;
}
}
};
const fetchSelfServeData = async (options: { immediate?: boolean } = {}) => {
const request = createSelfServeFetchRequest();
if (options.immediate) {
clearScheduledSelfServeFetch(null);
return executeSelfServeFetch(request);
}
if (!request || isSelfServeFetchUnmounted) {
clearScheduledSelfServeFetch(null);
return null;
}
return new Promise((resolve) => {
selfServeFetchResolvers.push(resolve);
if (selfServeFetchDebounceTimer) {
window.clearTimeout(selfServeFetchDebounceTimer);
}
selfServeFetchDebounceTimer = window.setTimeout(async () => {
selfServeFetchDebounceTimer = null;
const latestRequest = createSelfServeFetchRequest();
const result = await executeSelfServeFetch(latestRequest);
resolvePendingSelfServeFetches(result);
}, SELF_SERVE_FETCH_DEBOUNCE_MS);
});
};
const SERVER_ACTIVE_WASH_ENDPOINT = "/modules/self-serve/lane/wash/my-active-wash";
@@ -1023,7 +1129,7 @@ const retrySelfServeData = async () => {
isSelfServeRetrying.value = true;
try {
await fetchSelfServeData();
await fetchSelfServeData({ immediate: true });
} finally {
isSelfServeRetrying.value = false;
}
@@ -1093,7 +1199,7 @@ const onVehicleStepNext = async () => {
return;
}
await fetchSelfServeData();
await fetchSelfServeData({ immediate: true });
await nextTick();
markQuestionReviewRequired();
currentStep.value = visibleQuestions.value.length > 0 ? steps.QUESTIONS : steps.SELECT_LANE;
@@ -1216,6 +1322,9 @@ onUnmounted(() => {
unregisterBeforeUnload.value();
}
isSelfServeFetchUnmounted = true;
clearScheduledSelfServeFetch(null);
selfServeFetchSequence += 1;
stopAutoRefresh();
stopActiveWashRefresh();
markDestroying();
+35
View File
@@ -419,6 +419,8 @@ describe("MyWashStart", () => {
});
it("wires child updates back into the self-serve runtime", async () => {
vi.useFakeTimers();
const wrapper = mountWithApp(MyWashStart, {
global: {
stubs: stubComponents,
@@ -430,6 +432,7 @@ describe("MyWashStart", () => {
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
await nextTick();
await vi.advanceTimersByTimeAsync(300);
await flushPromises();
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
@@ -449,6 +452,34 @@ describe("MyWashStart", () => {
});
});
it("debounces registration, lane, and vehicle type updates into one self-serve fetch", async () => {
vi.useFakeTimers();
const wrapper = mountWithApp(MyWashStart, {
global: {
stubs: stubComponents,
},
});
await flushPromises();
mocks.fetchSelfServeDataInternal.mockClear();
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
await nextTick();
await vi.advanceTimersByTimeAsync(299);
await flushPromises();
expect(mocks.fetchSelfServeDataInternal).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
await flushPromises();
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledTimes(1);
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
});
it("hides machine tasks while manual wash is selected", async () => {
mocks.activeTasks.value = [
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
@@ -597,6 +628,8 @@ describe("MyWashStart", () => {
});
it("falls back to available lane when restored lane is stale for the selected department", async () => {
vi.useFakeTimers();
mocks.restoredProgressPayload = {
washInProgress: false,
washLaneId: null,
@@ -619,6 +652,8 @@ describe("MyWashStart", () => {
},
});
await flushPromises();
await vi.advanceTimersByTimeAsync(300);
await flushPromises();
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");