use server sent events instead of polling
All checks were successful
Build Container / Build Container (push) Successful in 1m27s

This commit is contained in:
kritzl 2026-05-19 17:09:02 +02:00
commit 8281848215
Signed by: kritzl
SSH key fingerprint: SHA256:5BmINP9VjZWaUk5Z+2CTut1KFhwLtd0ZynMekKbtViM
2 changed files with 90 additions and 46 deletions

View file

@ -89,6 +89,23 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/api/locks/stream": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Watch Locks */
get: operations["watch_locks_api_locks_stream_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/locks/{lock_id}": { "/api/locks/{lock_id}": {
parameters: { parameters: {
query?: never; query?: never;
@ -135,7 +152,7 @@ export interface components {
* @description Statically known HTTP problem types using the [type URI scheme](https://datatracker.ietf.org/doc/rfc4151/) * @description Statically known HTTP problem types using the [type URI scheme](https://datatracker.ietf.org/doc/rfc4151/)
* @enum {string} * @enum {string}
*/ */
HttpProblemType: "type:noc@hamburg.ccc.de,2026:UNAUTHORIZED" | "type:noc@hamburg.ccc.de,2026:LOCK_NOT_FOUND"; HttpProblemType: "type:noc@hamburg.ccc.de,2026:UNAUTHORIZED" | "type:noc@hamburg.ccc.de,2026,FORBIDDEN_TO_OPERATE" | "type:noc@hamburg.ccc.de,2026:LOCK_NOT_FOUND";
/** Lock */ /** Lock */
Lock: { Lock: {
/** Name */ /** Name */
@ -178,14 +195,17 @@ export interface components {
}; };
/** UserStatus */ /** UserStatus */
UserStatus: { UserStatus: {
/** Is Logged In */
is_logged_in: boolean;
/** Is Authorized */ /** Is Authorized */
is_authorized: boolean; is_authorized: boolean;
/** Guaranteed Session Until */ /**
guaranteed_session_until: string | null; * Guaranteed Session Until
* Format: date-time
*/
guaranteed_session_until: string;
/** Username */ /** Username */
username: string | null; username: string;
/** Ccchh Roles */
ccchh_roles: string[];
}; };
/** ValidationError */ /** ValidationError */
ValidationError: { ValidationError: {
@ -332,6 +352,35 @@ export interface operations {
}; };
}; };
}; };
watch_locks_api_locks_stream_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"text/event-stream": unknown;
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"text/event-stream": components["schemas"]["HttpProblemDetail"];
};
};
};
};
operate_lock_api_locks__lock_id__patch: { operate_lock_api_locks__lock_id__patch: {
parameters: { parameters: {
query?: never; query?: never;
@ -365,6 +414,15 @@ export interface operations {
"application/json": components["schemas"]["HttpProblemDetail"]; "application/json": components["schemas"]["HttpProblemDetail"];
}; };
}; };
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HttpProblemDetail"];
};
};
/** @description Not Found */ /** @description Not Found */
404: { 404: {
headers: { headers: {

View file

@ -1,5 +1,5 @@
import {Fetcher} from "openapi-typescript-fetch" import {Fetcher} from "openapi-typescript-fetch"
import type {paths} from "../api/schema" import type {components, paths} from "../api/schema"
import type {ui} from "../i18n/ui.ts" import type {ui} from "../i18n/ui.ts"
const fetcher = Fetcher.for<paths>() const fetcher = Fetcher.for<paths>()
@ -29,7 +29,7 @@ declare global {
lang: keyof typeof ui; lang: keyof typeof ui;
doors: Array<DoorType>; doors: Array<DoorType>;
auth: AuthType; auth: AuthType;
doorAction: (action: 'unlock' | 'lock', doorId: string) => void; doorAction: (action: "unlock" | "lock", doorId: string) => void;
} }
} }
@ -105,18 +105,18 @@ async function checkUser() {
if (e instanceof getUserInfo.Error) { if (e instanceof getUserInfo.Error) {
const error = e.getActualType() const error = e.getActualType()
if (error.status === 401) {
if (!auth.recentLogout) if (error.status >= 500 && error.status < 600) {
auth.recentLogout = auth.authenticated // set recentLogout true, if user was logged in before
auth.authenticated = false
auth.authorized = false
auth.until = null
auth.username = ""
} else if (error.status >= 500 && error.status < 600) {
apiError.current = "serverError" apiError.current = "serverError"
} else {
console.error("unknown error:", error)
} }
if (!auth.recentLogout)
auth.recentLogout = auth.authenticated // set recentLogout true, if user was logged in before
auth.authenticated = false
auth.authorized = false
auth.until = null
auth.username = ""
} }
} finally { } finally {
localStorage.setItem("auth", JSON.stringify(auth)) localStorage.setItem("auth", JSON.stringify(auth))
@ -125,15 +125,24 @@ async function checkUser() {
} }
} }
async function fetchDoors() { async function subscribeDoorEvents() {
if (doors.length === 0) { if (doors.length === 0) {
loading.doors = true loading.doors = true
} }
refresh() refresh()
const getDoors = fetcher.path("/api/locks/").method("get").create()
try { const evtSource = new EventSource("/api/locks/stream")
const {data: doorInfo} = await getDoors({})
evtSource.onerror = () => {
if (!window.navigator.onLine) {
apiError.current = "networkError"
} else {
apiError.current = "serverError"
}
}
evtSource.onmessage = (event) => {
const doorInfo: Array<components["schemas"]["Lock"]> = JSON.parse(event.data)
apiError.current = null apiError.current = null
while (doors.length) { while (doors.length) {
@ -164,29 +173,6 @@ async function fetchDoors() {
loading.doors = false loading.doors = false
refresh() refresh()
} catch (e) {
// check which operation threw the exception
if (e instanceof getDoors.Error) {
const error = e.getActualType()
if (error.status === 401) {
console.log("unauthorized")
loading.doors = false
refresh()
} else if (error.status >= 500 && error.status < 600) {
apiError.current = "serverError"
clearInterval(doorsInterval)
} else {
console.error("unknown error:", error)
}
}
if (e instanceof Error) {
switch (e.name) {
case "TypeError":
apiError.current = "networkError"
}
}
} }
} }
@ -295,7 +281,7 @@ function refresh() {
loadAuthFromLocalStorage() loadAuthFromLocalStorage()
const doorsInterval = setInterval(fetchDoors, 250) // TODO: replace with SSE subscribeDoorEvents()
checkUser() checkUser()
document.addEventListener("loadeddata", () => { document.addEventListener("loadeddata", () => {