feat:#22 refactor to cache room schedule and webworker config

This commit is contained in:
survellow
2024-08-02 23:55:58 +02:00
parent e4bde20397
commit 4fcfa76e1c
14 changed files with 164 additions and 83 deletions

View File

@ -29,6 +29,7 @@ type RoomOccupancy struct {
type RoomOccupancyList struct { type RoomOccupancyList struct {
Start time.Time `bson:"start"` Start time.Time `bson:"start"`
Updated time.Time `bson:"updated"`
Granularity int `bson:"granularity"` Granularity int `bson:"granularity"`
Blocks int `bson:"blocks"` Blocks int `bson:"blocks"`
Rooms []RoomOccupancy `bson:"rooms"` Rooms []RoomOccupancy `bson:"rooms"`

View File

@ -119,21 +119,6 @@ paths:
/api/schedule/rooms: /api/schedule/rooms:
get: get:
summary: Get Room Occupancy summary: Get Room Occupancy
parameters:
- name: from
in: query
description: date
example: "2024-12-24T00:00:00.000Z"
required: true
schema:
type: string
- name: to
in: query
description: date
example: "2024-12-25T00:00:00.000Z"
required: true
schema:
type: string
responses: responses:
'200': '200':
description: Successful response description: Successful response
@ -294,6 +279,9 @@ components:
start: start:
type: string type: string
format: date-time format: date-time
updated:
type: string
format: date-time
granularity: granularity:
type: integer type: integer
blocks: blocks:
@ -306,9 +294,9 @@ components:
occupancy: occupancy:
type: string type: string
format: binary format: binary
required: required:
- name - name
- occupancy - occupancy
required: required:
- start - start
- granularity - granularity

View File

@ -181,9 +181,7 @@ func AddRoutes(app *pocketbase.PocketBase) {
Method: http.MethodGet, Method: http.MethodGet,
Path: "/api/schedule/rooms", Path: "/api/schedule/rooms",
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
from := c.QueryParam("from") rooms, err := room.GetRoomOccupancyList(app, RoomOccupancyGranularity)
to := c.QueryParam("to")
rooms, err := room.GetRoomOccupancyList(app, from, to, RoomOccupancyGranularity)
if err != nil { if err != nil {
slog.Error("Failed to get room occupancy: %v", "error", err) slog.Error("Failed to get room occupancy: %v", "error", err)

View File

@ -16,15 +16,41 @@
package functions package functions
import "time" import (
"time"
)
const START_OF_SUMMER_SEMESTER_MONTH = time.April
const START_OF_WINTER_SEMESTER_MONTH = time.October
// GetCurrentSemesterString returns the current semester as string // GetCurrentSemesterString returns the current semester as string
// if current month is between 10 and 03 -> winter semester "ws" // if current month is between 10 and 03 -> winter semester "ws"
func GetCurrentSemesterString() string { func GetCurrentSemesterString() string {
if time.Now().Month() >= 10 || time.Now().Month() <= 3 { if now := time.Now(); isBeforeSummerSemester(now) || isAfterSummerSemester(now) {
return "ws" return "ws"
} else { } else {
return "ss" return "ss"
} }
} }
// GetSemesterStart gibt das Startdatum des aktuellen Semesters zurück
func GetSemesterStart(date time.Time) time.Time {
if isBeforeSummerSemester(date) {
return time.Date(date.Year()-1, START_OF_WINTER_SEMESTER_MONTH, 1, 0, 0, 0, 0, date.Location())
} else if isAfterSummerSemester(date) {
return time.Date(date.Year(), START_OF_WINTER_SEMESTER_MONTH, 1, 0, 0, 0, 0, date.Location())
} else {
return time.Date(date.Year(), START_OF_SUMMER_SEMESTER_MONTH, 1, 0, 0, 0, 0, date.Location())
}
}
// Check if the given date is before the start of summer semester
func isBeforeSummerSemester(date time.Time) bool {
return date.Month() < START_OF_SUMMER_SEMESTER_MONTH
}
// Check if the given date is after the end of summer semester
func isAfterSummerSemester(date time.Time) bool {
return date.Month() >= START_OF_WINTER_SEMESTER_MONTH
}

View File

@ -68,16 +68,11 @@ func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to st
* @return room occupancy list * @return room occupancy list
* @return error if the database query fails * @return error if the database query fails
*/ */
func GetRoomOccupancyList(app *pocketbase.PocketBase, from string, to string, granularity int) (model.RoomOccupancyList, error) { func GetRoomOccupancyList(app *pocketbase.PocketBase, granularity int) (model.RoomOccupancyList, error) {
// try parsing the time strings
fromTime, err := time.Parse(time.RFC3339, from) now := time.Now()
if err != nil { fromTime := functions.GetSemesterStart(now)
return model.RoomOccupancyList{}, err toTime := functions.GetSemesterStart(now.AddDate(0, 6, 0))
}
toTime, err := time.Parse(time.RFC3339, to)
if err != nil {
return model.RoomOccupancyList{}, err
}
// calculate the number of blocks for the given time range and granularity // calculate the number of blocks for the given time range and granularity
timeDifference := toTime.Sub(fromTime) timeDifference := toTime.Sub(fromTime)
@ -142,6 +137,7 @@ func getRelevantRooms(app *pocketbase.PocketBase) ([]string, error) {
func emptyRoomOccupancyList(from time.Time, granularity int, blockCount int) model.RoomOccupancyList { func emptyRoomOccupancyList(from time.Time, granularity int, blockCount int) model.RoomOccupancyList {
return model.RoomOccupancyList{ return model.RoomOccupancyList{
Start: from, Start: from,
Updated: time.Now(),
Granularity: granularity, Granularity: granularity,
Blocks: blockCount, Blocks: blockCount,
Rooms: []model.RoomOccupancy{}, Rooms: []model.RoomOccupancy{},

View File

@ -16,7 +16,6 @@
import { BSON } from "bson"; import { BSON } from "bson";
import { RoomOccupancyList } from "@/model/roomOccupancyList.ts"; import { RoomOccupancyList } from "@/model/roomOccupancyList.ts";
import { addMonths } from "date-fns";
import { formatYearMonthDay } from "@/helpers/dates"; import { formatYearMonthDay } from "@/helpers/dates";
const END_OF_SUMMER_SEMESTER = "0930"; const END_OF_SUMMER_SEMESTER = "0930";
@ -59,32 +58,19 @@ export function getSemesterStart(date: Date): Date {
/** /**
* Fetches the room occupancy for a given date range. * Fetches the room occupancy for a given date range.
* @param from_date the start date of the date range * @returns RoomOccupancyList - the room occupancy list containing all rooms
* @param to_date the end date of the date range
* @returns RoomOccupancyList - the room occupancy list
*/ */
export async function fetchRoomOccupancy( export async function fetchRoomOccupancy(): Promise<RoomOccupancyList> {
from_date?: string,
to_date?: string,
): Promise<RoomOccupancyList> {
if (from_date == undefined) {
const new_from_date = getSemesterStart(new Date());
from_date = new_from_date.toISOString();
}
if (to_date == undefined) {
const new_to_date = getSemesterStart(addMonths(new Date(), 6));
to_date = new_to_date.toISOString();
}
let roomOccupancyList: RoomOccupancyList = new RoomOccupancyList( let roomOccupancyList: RoomOccupancyList = new RoomOccupancyList(
new Date(), new Date(),
new Date(2000, 0, 1),
0, 0,
0, 0,
[], [],
); );
await fetch("/api/schedule/rooms?from=" + from_date + "&to=" + to_date) await fetch("/api/schedule/rooms")
.then((response) => { .then((response) => {
return response.arrayBuffer(); return response.arrayBuffer();
}) })

View File

@ -27,8 +27,6 @@ import {useI18n} from "vue-i18n";
import allLocales from "@fullcalendar/core/locales-all"; import allLocales from "@fullcalendar/core/locales-all";
import router from "@/router"; import router from "@/router";
import {formatYearMonthDay} from "@/helpers/dates"; import {formatYearMonthDay} from "@/helpers/dates";
import {useQuery} from "@tanstack/vue-query";
import {fetchRoomOccupancy} from "@/api/fetchRoomOccupancy";
import {isValid} from "date-fns"; import {isValid} from "date-fns";
import {RoomOccupancyList} from "@/model/roomOccupancyList"; import {RoomOccupancyList} from "@/model/roomOccupancyList";
@ -39,6 +37,10 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
occupancy: {
type: RoomOccupancyList,
required: true,
},
}); });
const date: Ref<Date> = ref(new Date()); const date: Ref<Date> = ref(new Date());
@ -56,6 +58,7 @@ function setDateFromQuery() {
const day = queryDate.substring(6, 8); const day = queryDate.substring(6, 8);
date.value = new Date(`${year}-${month}-${day}`); date.value = new Date(`${year}-${month}-${day}`);
if (!isValid(date.value)) { if (!isValid(date.value)) {
console.error("Invalid date in URL, using current date instead.");
date.value = new Date(); date.value = new Date();
} }
} }
@ -77,6 +80,9 @@ const selectedRoom = computed(() => props.room);
* @returns Anonymized occupancy events * @returns Anonymized occupancy events
*/ */
function transformData(data: RoomOccupancyList) { function transformData(data: RoomOccupancyList) {
if (!data || !selectedRoom.value || !currentDateFrom.value || !currentDateTo.value) {
return [];
}
return data return data
.decodeOccupancy( .decodeOccupancy(
selectedRoom.value, selectedRoom.value,
@ -89,15 +95,9 @@ function transformData(data: RoomOccupancyList) {
})); }));
} }
const { data: occupancy } = useQuery({
queryKey: ["roomOccupancy"], //, selectedRoom, currentDateFrom, currentDateTo],
queryFn: () => fetchRoomOccupancy(),
staleTime: 12 * 3600000, // 12 hours
});
const occupations = computed(() => { const occupations = computed(() => {
if (!occupancy.value) return; if (!props.occupancy) return;
return transformData(occupancy.value); return transformData(props.occupancy);
}); });
watch(occupations, () => fullCalendar.value?.getApi().refetchEvents()); watch(occupations, () => fullCalendar.value?.getApi().refetchEvents());

View File

@ -34,6 +34,56 @@ function setup() {
de, de,
ja, ja,
}, },
datetimeFormats: {
en: {
short: {
year: "numeric",
month: "short",
day: "numeric",
},
long: {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
hour: "numeric",
minute: "numeric",
hour12: true,
},
},
de: {
short: {
year: "numeric",
month: "short",
day: "numeric",
},
long: {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
hour: "numeric",
minute: "numeric",
hour12: false,
},
},
ja: {
short: {
year: "numeric",
month: "short",
day: "numeric",
},
long: {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
hour: "numeric",
minute: "numeric",
hour12: true,
},
},
},
}); });
return _i18n; return _i18n;
} }

View File

@ -33,7 +33,9 @@
"noRoomsAvailable": "Keine Räume verfügbar", "noRoomsAvailable": "Keine Räume verfügbar",
"available": "verfügbar", "available": "verfügbar",
"occupied": "belegt", "occupied": "belegt",
"stub": "bitte online prüfen" "stub": "bitte online prüfen",
"lastUpdate": "Zeitpunkt der letzten Aktualisierung",
"noData": "Es konnten keine Daten geladen werden."
}, },
"freeRooms": { "freeRooms": {
"freeRooms": "Freie Räume", "freeRooms": "Freie Räume",

View File

@ -33,7 +33,9 @@
"noRoomsAvailable": "no rooms listed", "noRoomsAvailable": "no rooms listed",
"available": "available", "available": "available",
"occupied": "occupied", "occupied": "occupied",
"stub": "please check online" "stub": "please check online",
"lastUpdate": "time of last update",
"noData": "could not load data"
}, },
"freeRooms": { "freeRooms": {
"freeRooms": "free rooms", "freeRooms": "free rooms",

View File

@ -33,7 +33,9 @@
"noRoomsAvailable": "利用可能な部屋がありません", "noRoomsAvailable": "利用可能な部屋がありません",
"available": "利用可能", "available": "利用可能",
"occupied": "占有中", "occupied": "占有中",
"stub": "オンラインでご確認ください。" "stub": "オンラインでご確認ください。",
"lastUpdate": "最終更新時刻",
"noData": "データをロードできませんでした。"
}, },
"freeRooms": { "freeRooms": {
"freeRooms": "空いている部屋", "freeRooms": "空いている部屋",

View File

@ -65,6 +65,7 @@ class RoomOccupancy {
export class RoomOccupancyList { export class RoomOccupancyList {
constructor( constructor(
public start: Date, public start: Date,
public updated: Date,
public granularity: number, public granularity: number,
public blocks: number, public blocks: number,
public rooms: RoomOccupancy[], public rooms: RoomOccupancy[],
@ -312,6 +313,7 @@ export class RoomOccupancyList {
public static fromJSON(json: Document): RoomOccupancyList { public static fromJSON(json: Document): RoomOccupancyList {
return new RoomOccupancyList( return new RoomOccupancyList(
json.start, json.start,
json.updated,
json.granularity, json.granularity,
json.blocks, json.blocks,
json.rooms.map( json.rooms.map(

View File

@ -18,16 +18,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script lang="ts" setup> <script lang="ts" setup>
import { Ref, computed, ref, watch } from "vue"; import { Ref, computed, ref, watch } from "vue";
import { fetchRoom } from "../api/fetchRoom.ts";
import DynamicPage from "./DynamicPage.vue"; import DynamicPage from "./DynamicPage.vue";
import RoomOccupationOffline from "../components/RoomOccupationOffline.vue"; import RoomOccupationOffline from "../components/RoomOccupationOffline.vue";
import { computedAsync } from "@vueuse/core";
import router from "@/router"; import router from "@/router";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
import { fetchRoomOccupancy } from "@/api/fetchRoomOccupancy.ts";
type Room = { type Room = {
name: string; name: string;
}; };
const queryClient = useQueryClient();
const { data: occupancy, dataUpdatedAt: updateDate } = useQuery({
queryKey: ["roomOccupancy"],
queryFn: () => fetchRoomOccupancy(),
staleTime: 12 * 3600000, // 12 hours
});
// Manually refetch the room occupancy data
function refetchRoomOccupancy() {
queryClient.invalidateQueries({ queryKey: ["roomOccupancy"] });
}
const selectedRoom: Ref<Room> = ref({ name: "" }); const selectedRoom: Ref<Room> = ref({ name: "" });
// Watch for changes in URL parameter // Watch for changes in URL parameter
@ -38,21 +51,15 @@ router.afterEach(async (to) => {
} }
}); });
const rooms = computedAsync<Set<string>>(async () => { const rooms = computed<Set<string>>(() => {
let rooms: Set<string> = new Set(); let rooms: Set<string> = new Set();
return await fetchRoom()
.then((data) => { occupancy.value?.rooms.forEach((room) => {
rooms = new Set(data); rooms.add(room.name);
return rooms; });
})
.finally(() => { return rooms;
const room = router.currentRoute.value.query.room; });
if (room && typeof room === "string") {
// check if room is available in roomsList
setRoomFromList(room, rooms);
}
});
}, new Set());
const roomsList = computed(() => { const roomsList = computed(() => {
return Array.from(rooms.value).map((room) => { return Array.from(rooms.value).map((room) => {
@ -102,7 +109,17 @@ watch(selectedRoom, (newRoom: Room) => {
/> />
</template> </template>
<template #content> <template #content>
<RoomOccupationOffline :room="selectedRoom.name" /> <RoomOccupationOffline v-if="occupancy" :room="selectedRoom.name" :occupancy="occupancy"/>
</template> <div v-else>
<p>{{ $t('roomFinderPage.noData') }}</p>
</div>
<!--last update date-->
<div class="flex align-items-baseline justify-content-end text-right text-xs text-gray-500">
<span v-if="occupancy">test: {{ $d(occupancy.updated, 'long') }}</span>
<span>{{ $t("roomFinderPage.lastUpdate") }}: {{ $d(new Date(updateDate), 'long') }}</span>
<Button size="small" @click="refetchRoomOccupancy" icon="pi pi-refresh" class="p-button-text p-button-sm" />
</div>
</template>
</DynamicPage> </DynamicPage>
</template> </template>

View File

@ -82,7 +82,7 @@ export default defineConfig({
}, },
registerType: "autoUpdate", registerType: "autoUpdate",
workbox: { workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}"], globPatterns: ["**/*.{js,css,html,ico,png,svg,json,vue,txt,woff2}", "/api/schedule/rooms"],
cleanupOutdatedCaches: true, cleanupOutdatedCaches: true,
runtimeCaching: [ runtimeCaching: [
{ {
@ -96,6 +96,17 @@ export default defineConfig({
}, },
}, },
}, },
{
urlPattern: ("/api/schedule/rooms"),
method: "GET",
handler: "StaleWhileRevalidate",
options: {
cacheName: "room-schedule-cache",
expiration: {
maxAgeSeconds: 12 * 60 * 60, // 12 hours
},
},
},
{ {
urlPattern: /^https?.*/, urlPattern: /^https?.*/,
handler: "NetworkFirst", handler: "NetworkFirst",