diff --git a/app.vue b/app.vue
index a8c7190..afa34bb 100644
--- a/app.vue
+++ b/app.vue
@@ -11,7 +11,10 @@
import "~/assets/global.css";
const event = useRequestEvent();
const sessionStore = useSessionStore();
-await callOnce("fetch-session", async () => {
+const eventsStore = useEventsStore();
+const nuxtApp = useNuxtApp();
+await callOnce("fetch-globals", async () => {
await sessionStore.fetch(event);
+ await nuxtApp.runWithContext(eventsStore.fetchLastEventId);
})
diff --git a/components/DiffSchedule.vue b/components/DiffSchedule.vue
index 3175d5a..5bc2541 100644
--- a/components/DiffSchedule.vue
+++ b/components/DiffSchedule.vue
@@ -61,7 +61,7 @@ const shifts = computed(() => {
});
-
diff --git a/components/TableUsers.vue b/components/TableUsers.vue
index 789f902..7021206 100644
--- a/components/TableUsers.vue
+++ b/components/TableUsers.vue
@@ -61,7 +61,3 @@
useEventSource();
const usersStore = useUsersStore();
-
-
diff --git a/composables/event-source.ts b/composables/event-source.ts
index 838b879..0e06b17 100644
--- a/composables/event-source.ts
+++ b/composables/event-source.ts
@@ -2,12 +2,12 @@
SPDX-FileCopyrightText: © 2025 Hornwitser
SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { ApiEvent } from "~/shared/types/api";
+import type { ApiEvent, ApiEventStreamMessage } from "~/shared/types/api";
interface AppEventMap {
"open": Event,
- "message": MessageEvent,
- "update": MessageEvent,
+ "message": MessageEvent,
+ "event": MessageEvent,
"error": Event,
"close": Event,
}
@@ -18,12 +18,11 @@ class AppEventSource extends EventTarget {
#forwardEvent(type: string) {
this.#source!.addEventListener(type, event => {
- if (type === "open" || type === "message" || type === "error") {
- console.log("AppEventSource", event.type, event.data);
+ console.log("AppEventSource", event.type, event.data);
+ if (type === "open" || type === "error") {
this.dispatchEvent(new Event(event.type));
- } else {
- const data = event.data ? JSON.parse(event.data) as ApiEvent : undefined;
- console.log("AppEventSource", event.type, data);
+ } else if (type === "message") {
+ const data = event.data ? JSON.parse(event.data) as ApiEventStreamMessage : undefined;
if (data?.type === "connected") {
this.#sourceSessionId = data.session?.id;
}
@@ -34,17 +33,27 @@ class AppEventSource extends EventTarget {
source: event.source,
ports: [...event.ports],
}));
+ } else {
+ const data = event.data ? JSON.parse(event.data) as ApiEvent : undefined;
+ this.dispatchEvent(new MessageEvent(event.type, {
+ data,
+ origin: event.origin,
+ lastEventId: event.lastEventId,
+ source: event.source,
+ ports: [...event.ports],
+ }));
}
});
}
- open(sessionId: number | undefined) {
+ open(sessionId: number | undefined, lastEventId: number) {
console.log("Opening event source sid:", sessionId);
this.#sourceSessionId = sessionId;
- this.#source = new EventSource("/api/events");
+ const query = new URLSearchParams({ lastEventId: String(lastEventId) });
+ this.#source = new EventSource(`/api/events?${query}`);
this.#forwardEvent("open");
this.#forwardEvent("message");
- this.#forwardEvent("update");
+ this.#forwardEvent("event");
this.#forwardEvent("error");
}
@@ -58,20 +67,20 @@ class AppEventSource extends EventTarget {
}
#connectRefs = 0;
- connect(sessionId: number | undefined) {
+ connect(sessionId: number | undefined, lastEventId: number) {
this.#connectRefs += 1;
if (this.#source && this.#sourceSessionId !== sessionId) {
this.close();
}
if (!this.#source) {
- this.open(sessionId);
+ this.open(sessionId, lastEventId);
}
}
- reconnect(sessionId: number | undefined) {
+ reconnect(sessionId: number | undefined, lastEventId: number) {
if (this.#source && this.#sourceSessionId !== sessionId) {
this.close();
- this.open(sessionId);
+ this.open(sessionId, lastEventId);
}
}
@@ -113,14 +122,15 @@ export const appEventSource = import.meta.client ? new AppEventSource() : null;
export function useEventSource() {
const sessionStore = useSessionStore();
+ const eventsStore = useEventsStore();
onMounted(() => {
console.log("useEventSource onMounted", sessionStore.id);
- appEventSource!.connect(sessionStore.id);
+ appEventSource!.connect(sessionStore.id, eventsStore.lastEventId);
})
watch(() => sessionStore.id, () => {
console.log("useEventSource sessionStore.id change", sessionStore.id);
- appEventSource!.reconnect(sessionStore.id);
+ appEventSource!.reconnect(sessionStore.id, eventsStore.lastEventId);
})
onUnmounted(() => {
diff --git a/docs/dev/server-sent-events.md b/docs/dev/server-sent-events.md
index 82ff99b..d510c1e 100644
--- a/docs/dev/server-sent-events.md
+++ b/docs/dev/server-sent-events.md
@@ -11,3 +11,9 @@ To update in real time this application sends a `text/event-source` stream using
Upon connecting a `"connect"` event is emitted with the session the connection was made under. This is the primary mechanism a user agent discovers its own session having been rotated into a new one, which also happens when the access level of the account associated with the session changes.
After the `"connect"` event the user agent will start to receive updates to resources it has access to that has changed. There is no filtering for what resoucres the user agent receives updates for at the moment as there's not enough events to justify the complexity of server-side subscriptions and filtering.
+
+## Id and order
+
+Events are guaranteed to be delivered in order, and to maintain consistency the server provides the following guarantee: Any entities fetched after receiving a response from `/api/last-event-id` will include updates from all events up to and including the `id` received from the response.
+
+This means that a client can fetch an up to date and live representation of any API entity by first fetching the last event from `/api/last-event-id`, and then in parallel fetch any entities as well as opening the `/api/events` stream with the `lastEventId` query param set to the value received from the `/api/last-event-id` endpoint.
diff --git a/pages/admin/index.vue b/pages/admin/index.vue
index 116a189..86a1b5f 100644
--- a/pages/admin/index.vue
+++ b/pages/admin/index.vue
@@ -109,7 +109,3 @@ const tabs = [
{ id: "database", title: "Database" },
];
-
-
diff --git a/pages/admin/users/[id].vue b/pages/admin/users/[id].vue
index 0579b44..bf2355c 100644
--- a/pages/admin/users/[id].vue
+++ b/pages/admin/users/[id].vue
@@ -84,7 +84,7 @@ const { pending, data, error } = await useFetch(() => `/api/users/${id.value}/de
const userDetails = data as Ref;
-