405 back reload token with synctokens (#436)
* [#405] added syncToken in calendar params and fetch with sync token * [#405] changed reload to work with sync-token * [#405] fixup promise handling, added calendar adding and removing hanlding with refresh * [#405] fixed event expansion calls * [#405 & refactor] added helperfunction to get base event uid + refactored synctoken updates management * [#405] added pMap lib to process event expansion * [#405] added flag for no synctoken / new synctoken
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { formatReduxError } from "../../../utils/errorUtils";
|
||||
import { CalendarEvent } from "../../Events/EventsTypes";
|
||||
import { getCalendar } from "../CalendarApi";
|
||||
import { RejectedError } from "../CalendarSlice";
|
||||
import { extractCalendarEvents } from "../utils/extractCalendarEvents";
|
||||
|
||||
export const getCalendarDetailAsync = createAsyncThunk<
|
||||
{
|
||||
calId: string;
|
||||
events: CalendarEvent[];
|
||||
calType?: string;
|
||||
syncToken?: string;
|
||||
},
|
||||
{
|
||||
calId: string;
|
||||
match: { start: string; end: string };
|
||||
calType?: string;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
{ rejectValue: RejectedError }
|
||||
>(
|
||||
"calendars/getCalendarDetails",
|
||||
async ({ calId, match, calType, signal }, { rejectWithValue }) => {
|
||||
try {
|
||||
const calendar = (await getCalendar(calId, match, signal)) as any;
|
||||
|
||||
const color = calendar["apple:color"];
|
||||
const syncToken = calendar._embedded?.["sync-token"];
|
||||
|
||||
const items = calendar._embedded?.["dav:item"];
|
||||
const events: CalendarEvent[] = Array.isArray(items)
|
||||
? items.flatMap((item: any) =>
|
||||
extractCalendarEvents(item, { calId, color })
|
||||
)
|
||||
: [];
|
||||
|
||||
return { calId, events, calType, syncToken };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import { formatReduxError } from "../../../utils/errorUtils";
|
||||
import { getOpenPaasUser, getUserDetails } from "../../User/userAPI";
|
||||
import { getCalendars } from "../CalendarApi";
|
||||
import { RejectedError } from "../CalendarSlice";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { normalizeCalendar } from "../utils/normalizeCalendar";
|
||||
|
||||
export const getCalendarsListAsync = createAsyncThunk<
|
||||
{ importedCalendars: Record<string, Calendar>; errors: string },
|
||||
void,
|
||||
{ rejectValue: RejectedError; state: any }
|
||||
>("calendars/getCalendars", async (_, { rejectWithValue, getState }) => {
|
||||
const state = getState() as any;
|
||||
const existingCalendars = state.calendars.list || {};
|
||||
const existingUser = { id: state.user?.userData?.openpaasId || undefined };
|
||||
try {
|
||||
const fetchedCalendars: Record<string, Calendar> = {};
|
||||
const user = existingUser.id
|
||||
? existingUser
|
||||
: ((await getOpenPaasUser()) as Record<string, string>);
|
||||
const calendars = (await getCalendars(user.id)) as Record<string, any>;
|
||||
const rawCalendars = calendars._embedded["dav:calendar"] as Record<
|
||||
string,
|
||||
any
|
||||
>[];
|
||||
const errors: string[] = [];
|
||||
|
||||
const normalizedCalendars = rawCalendars.map((cal) =>
|
||||
normalizeCalendar(cal)
|
||||
);
|
||||
|
||||
const uniqueOwnerIds = Array.from(
|
||||
new Set(normalizedCalendars.map(({ ownerId }) => ownerId).filter(Boolean))
|
||||
);
|
||||
|
||||
const ownerDataMap = new Map<string, any>();
|
||||
const OWNER_BATCH_SIZE = 20;
|
||||
|
||||
const fetchOwnerData = async (ownerId: string) => {
|
||||
try {
|
||||
const data = await getUserDetails(ownerId);
|
||||
ownerDataMap.set(ownerId, data);
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to fetch user details for ${ownerId}:`, error);
|
||||
ownerDataMap.set(ownerId, {
|
||||
firstname: "",
|
||||
lastname: "Unknown User",
|
||||
emails: [],
|
||||
});
|
||||
errors.push(formatReduxError(error));
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < uniqueOwnerIds.length; i += OWNER_BATCH_SIZE) {
|
||||
const chunk = uniqueOwnerIds.slice(i, i + OWNER_BATCH_SIZE);
|
||||
await Promise.all(chunk.map((ownerId) => fetchOwnerData(ownerId)));
|
||||
}
|
||||
|
||||
normalizedCalendars.forEach(
|
||||
({ cal, description, delegated, link, id, ownerId, visibility }) => {
|
||||
const ownerData = ownerDataMap.get(ownerId) || {
|
||||
firstname: "",
|
||||
lastname: "Unknown User",
|
||||
emails: [],
|
||||
};
|
||||
const name =
|
||||
ownerId !== user.id && cal["dav:name"] === "#default"
|
||||
? `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}` +
|
||||
"'s calendar"
|
||||
: cal["dav:name"];
|
||||
|
||||
const color = {
|
||||
light: cal["apple:color"] ?? "#006BD8",
|
||||
dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
|
||||
};
|
||||
fetchedCalendars[id] = {
|
||||
id,
|
||||
name,
|
||||
link,
|
||||
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${ownerData.lastname}`,
|
||||
ownerEmails: ownerData.emails,
|
||||
description,
|
||||
delegated,
|
||||
color,
|
||||
visibility,
|
||||
events: {},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const importedCalendars: Record<string, Calendar> = {};
|
||||
|
||||
const fetchedIds = new Set(Object.keys(fetchedCalendars));
|
||||
const existingIds = new Set(Object.keys(existingCalendars));
|
||||
|
||||
const added = [...fetchedIds].filter((id) => !existingIds.has(id));
|
||||
|
||||
existingIds.forEach((id) => {
|
||||
if (fetchedIds.has(id)) {
|
||||
const existingCal = existingCalendars[id];
|
||||
const fetchedCal = fetchedCalendars[id];
|
||||
|
||||
if (fetchedCal) {
|
||||
importedCalendars[id] = {
|
||||
...fetchedCal,
|
||||
color: existingCal.color,
|
||||
events: existingCal.events || {},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add new calendars
|
||||
added.forEach((id) => {
|
||||
importedCalendars[id] = fetchedCalendars[id];
|
||||
});
|
||||
|
||||
return {
|
||||
importedCalendars,
|
||||
errors: errors.join("\n"),
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||
import pMap from "p-map";
|
||||
import { formatReduxError } from "../../../utils/errorUtils";
|
||||
import { CalendarEvent } from "../../Events/EventsTypes";
|
||||
import { fetchSyncTokenChanges } from "../api/fetchSyncTokenChanges";
|
||||
import { RejectedError } from "../CalendarSlice";
|
||||
import { Calendar } from "../CalendarTypes";
|
||||
import { expandEventFunction } from "../utils/expandEventFunction";
|
||||
import { processSyncUpdates } from "../utils/processSyncTokenUpdates";
|
||||
|
||||
export interface SyncTokenUpdates {
|
||||
calId: string;
|
||||
deletedEvents: string[];
|
||||
createdOrUpdatedEvents: CalendarEvent[];
|
||||
calType?: "temp";
|
||||
syncToken?: string;
|
||||
syncStatus?: string;
|
||||
}
|
||||
|
||||
export const refreshCalendarWithSyncToken = createAsyncThunk<
|
||||
SyncTokenUpdates,
|
||||
{
|
||||
calendar: Calendar;
|
||||
calType?: "temp";
|
||||
calendarRange: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
maxConcurrency?: number;
|
||||
},
|
||||
{
|
||||
rejectValue: RejectedError;
|
||||
}
|
||||
>(
|
||||
"calendars/refreshWithSyncToken",
|
||||
async (
|
||||
{ calendar, maxConcurrency = 8, calendarRange, calType },
|
||||
{ rejectWithValue }
|
||||
) => {
|
||||
try {
|
||||
if (!calendar?.syncToken) {
|
||||
return {
|
||||
calId: calendar.id,
|
||||
deletedEvents: [],
|
||||
createdOrUpdatedEvents: [],
|
||||
calType,
|
||||
syncStatus: "NO_SYNC_TOKEN",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetchSyncTokenChanges(calendar);
|
||||
const newSyncToken = response["sync-token"];
|
||||
const updates = response?._embedded?.["dav:item"] ?? [];
|
||||
|
||||
const { toDelete, toExpand } = processSyncUpdates(updates);
|
||||
|
||||
const createdOrUpdatedEvents = await pMap(
|
||||
toExpand,
|
||||
expandEventFunction(calendarRange, calendar),
|
||||
{ concurrency: maxConcurrency }
|
||||
);
|
||||
|
||||
return {
|
||||
calId: calendar.id,
|
||||
deletedEvents: toDelete,
|
||||
createdOrUpdatedEvents: createdOrUpdatedEvents
|
||||
.flat()
|
||||
.filter(Boolean) as CalendarEvent[],
|
||||
calType,
|
||||
syncToken: newSyncToken,
|
||||
syncStatus: newSyncToken ? "SUCCESS" : "NO_NEW_SYNC_TOKEN",
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue({
|
||||
message: formatReduxError(err),
|
||||
status: err.response?.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user