Last active
October 26, 2022 08:23
-
-
Save Lyokolux/67f66a8aa02aecdcf114d1cb78c85c62 to your computer and use it in GitHub Desktop.
Server Side Events implementation in a Nuxt 3 backend of notifications retrieved from the prisma ORM. A second file provide an example of use as snippet.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { NotificationEvent } from '~~/server/api/v1/user/notifications/live.get' | |
/** Init the listener on the dedicated API endpoint */ | |
const evtSource = new EventSource("/api/live") | |
/** Callback on message recieved */ | |
evtSource.onmessage = (e: MessageEvent<string>) => { | |
if (e.data) { | |
this.notifications = [...JSON.parse(e.data) as NotificationEvent[], ...this.notifications] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { CompatibilityEvent } from "h3" | |
import crypto from 'crypto' | |
import { getAuthContext, useAuthenticationGuard } from "~~/server/lib/auth" | |
import { db } from "~~/server/lib/prisma" | |
import { TimeInMS } from "~~/const/time" | |
/** Amount of time between two checks of new content */ | |
const CHECK_INTERVAL = 10 * TimeInMS.SECOND | |
/** | |
* Amount of intervals before closing the connection from the server. | |
* For example if the client does not respond anymore. | |
*/ | |
const INTERVALS_BEFORE_CLOSE = 10 | |
/** Content of the SSE as TS-Type */ | |
export type NotificationEvent = { | |
createdAt: Date | |
content: string | |
notificationTypeId: string | |
}[] | undefined | |
/** | |
* Expected format of an SSE Payload | |
*/ | |
const sendEvent = (e: CompatibilityEvent, id: string, data: any) => { | |
e.res.write(`id: ${id}\n`) | |
e.res.write(`data: ${data}\n\n`) | |
} | |
/** A dummy wait interval between checks */ | |
const sleep = (ms: number) => { | |
return new Promise((resolve) => setTimeout(resolve, ms)) | |
} | |
export default defineEventHandler(async (event) => { | |
const { isNotAuthenticatedResponse } = useAuthenticationGuard(event) | |
if (isNotAuthenticatedResponse) { | |
return isNotAuthenticatedResponse | |
} | |
// Init SSE connection with proper Content-Type and Connection | |
event.res.writeHead(200, { | |
'Cache-Control': 'no-cache', | |
Connection: 'keep-alive', | |
'Content-Type': 'text/event-stream', | |
}) | |
const userId = getAuthContext(event)?.email as string // defined here as there is an auth guard | |
let lastPull = new Date() | |
for (let cycleCount = 0; cycleCount < INTERVALS_BEFORE_CLOSE; cycleCount++) { | |
// 1. Pull the new notifications | |
let notifications = await db.notification.findMany({ | |
select: { content: true, notificationTypeId: true, createdAt: true }, | |
where: { | |
toUserId: userId, | |
createdAt: { | |
gte: lastPull, | |
} | |
}, | |
orderBy: { | |
createdAt: 'desc' | |
}, | |
}) | |
lastPull = new Date() | |
// 2. Prepare the payload it there are new data | |
if (notifications.length > 0) { | |
const payload = JSON.stringify(notifications) | |
// 3. Send it | |
const sseId = crypto.randomUUID() // generate a random ID for the SSE | |
sendEvent(event, sseId, payload) // send a SSE with utility function | |
} | |
// 4. Wait stupidely and check again after a period of time | |
await sleep(CHECK_INTERVAL) | |
} | |
// close the connection after INTERVALS_BEFORE_CLOSE amount of intervals | |
event.res.end() | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment