Files
pleno-vue/src/composables/useOrderMetadataAutosave.js
T

150 lines
3.3 KiB
JavaScript

import { computed, onBeforeUnmount, ref, watch } from "vue";
export const ORDER_METADATA_AUTOSAVE_DELAY_MS = 600;
const defaultNormalizeValue = (value) => (value == null ? "" : String(value));
const resolveSourceValue = (source) => {
if (typeof source === "function") {
return source();
}
return source?.value;
};
export const useOrderMetadataAutosave = ({
source,
saveValue,
normalizeValue = defaultNormalizeValue,
delay = ORDER_METADATA_AUTOSAVE_DELAY_MS,
onSaved = null,
onError = null,
}) => {
const draft = ref(normalizeValue(resolveSourceValue(source)));
const lastSavedValue = ref(normalizeValue(resolveSourceValue(source)));
const isSaving = ref(false);
const isDirty = computed(() => normalizeValue(draft.value) !== normalizeValue(lastSavedValue.value));
let timerId = null;
let saveAgainAfterCurrentRequest = false;
let nextSequence = 0;
let lastAppliedSequence = 0;
const clearPendingSave = () => {
if (timerId !== null) {
window.clearTimeout(timerId);
timerId = null;
}
};
const syncFromSource = (value) => {
const normalizedValue = normalizeValue(value);
const shouldHydrateDraft = normalizeValue(draft.value) === normalizeValue(lastSavedValue.value);
lastSavedValue.value = normalizedValue;
if (shouldHydrateDraft) {
draft.value = normalizedValue;
}
};
watch(() => resolveSourceValue(source), syncFromSource, { immediate: true });
const persist = async () => {
clearPendingSave();
if (!isDirty.value) {
return false;
}
if (isSaving.value) {
saveAgainAfterCurrentRequest = true;
return false;
}
const requestedValue = normalizeValue(draft.value);
const sequence = ++nextSequence;
let saveCompleted = false;
isSaving.value = true;
try {
const result = await saveValue(requestedValue);
const savedValue = normalizeValue(result ?? requestedValue);
if (sequence >= lastAppliedSequence) {
lastAppliedSequence = sequence;
lastSavedValue.value = savedValue;
if (normalizeValue(draft.value) === requestedValue) {
draft.value = savedValue;
}
}
if (typeof onSaved === "function") {
await onSaved(savedValue, requestedValue);
}
saveCompleted = true;
return true;
} catch (error) {
if (typeof onError === "function") {
onError(error);
} else {
console.error(error);
}
return false;
} finally {
isSaving.value = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
}
}
};
const scheduleSave = () => {
clearPendingSave();
if (!isDirty.value) {
return;
}
timerId = window.setTimeout(() => {
void persist();
}, delay);
};
const flush = async () => {
return persist();
};
onBeforeUnmount(() => {
clearPendingSave();
if (isSaving.value) {
saveAgainAfterCurrentRequest = true;
return;
}
if (isDirty.value) {
void persist();
}
});
return {
draft,
lastSavedValue,
isDirty,
isSaving,
scheduleSave,
flush,
clearPendingSave,
syncFromSource,
};
};