owltide/stores/schedules.ts

111 lines
3.3 KiB
TypeScript
Raw Normal View History

/*
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { Info } from "~/shared/utils/luxon";
interface SyncOperation {
controller: AbortController,
promise: Promise<Ref<ClientSchedule>>,
}
export const useSchedulesStore = defineStore("schedules", () => {
const sessionStore = useSessionStore();
const accountStore = useAccountStore();
const state = {
activeScheduleId: ref<number | undefined>(111),
schedules: ref<Map<number, Ref<ClientSchedule>>>(new Map()),
pendingSyncs: ref<Map<number, SyncOperation>>(new Map()),
};
const getters = {
activeSchedule: computed(() => {
if (state.activeScheduleId.value === undefined)
throw Error("No active schedule");
const schedule = state.schedules.value.get(state.activeScheduleId.value);
if (!schedule)
throw Error("Active schedule has not been fetched");
return schedule;
}),
};
const actions = {
async fetch(id: number) {
if (id !== 111) { throw Error("invalid id"); }
console.log("schedules store fetch", id);
const schedule = state.schedules.value.get(id);
if (schedule) {
console.log("return cached");
return schedule;
}
const pending = state.pendingSyncs.value.get(id);
if (pending) {
console.log("return pending");
return pending.promise;
}
console.log("return new fetch");
const requestFetch = useRequestFetch();
const controller = new AbortController();
const zone = Info.normalizeZone(accountStore.activeTimezone);
const locale = accountStore.activeLocale;
const promise = (async () => {
try {
const apiSchedule = await requestFetch("/api/schedule", { signal: controller.signal });
if (apiSchedule.deleted) {
throw new Error("Unexpecetd deleted schedule");
}
const schedule = ref(ClientSchedule.fromApi(apiSchedule, { zone, locale })) as Ref<ClientSchedule>;
state.schedules.value.set(id, schedule);
state.pendingSyncs.value.delete(id);
return schedule;
} catch (err: any) {
if (err.name !== "AbortError")
state.pendingSyncs.value.delete(id);
throw err;
}
})();
state.pendingSyncs.value.set(id, {
controller,
promise,
});
return promise;
},
async resync(id: number) {
if (id !== 111) { throw Error("invalid id"); }
const pending = state.pendingSyncs.value.get(id);
if (pending) {
pending.controller.abort();
}
state.schedules.value.delete(id);
state.pendingSyncs.value.delete(id);
await actions.fetch(id);
},
}
watch(() => sessionStore.id, (id, oldId) => {
for (const [scheduleId, pending] of state.pendingSyncs.value) {
console.log("Aborting pending schedule sync", scheduleId, "due session.id change from", oldId, "to", id);
pending.controller.abort();
state.pendingSyncs.value.delete(scheduleId);
}
})
appEventSource?.addEventListener("update", (event) => {
if (event.data.type !== "schedule-update") {
return;
}
const schedule = state.schedules.value.get(111);
const update = event.data.data;
// XXX validate updatedFrom/updatedAt here
if (schedule && !schedule.value.deleted && !update.deleted) {
const zone = Info.normalizeZone(accountStore.activeTimezone);
const locale = accountStore.activeLocale;
schedule.value.apiUpdate(update, { zone, locale })
}
});
return { ...state, ...getters, ...actions };
});