From ca79b4a20dd63a94e0b7237f662b5693ca70c090 Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Thu, 13 Jun 2024 12:03:07 +0200 Subject: [PATCH] feat: :sparkles: add server sessions --- src/session/mod.ts | 142 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/session/mod.ts diff --git a/src/session/mod.ts b/src/session/mod.ts new file mode 100644 index 0000000..28748b4 --- /dev/null +++ b/src/session/mod.ts @@ -0,0 +1,142 @@ +import { getCookies } from '@std/http/cookie' + +type SessionEntry = { + accessCount: number + flash: boolean + value: T +} + +export class SessionStore { + static #store = new Map() + static createSession(): Session { + return new Session(this.#store) + } + + static getSession(uuid: string): Session | undefined { + // Check session validity + const halfHour = 30 * 60 * 1_000 + const maxAge = Date.now() - halfHour + const session = this.#store.get(uuid) + + if (session === undefined) { + return undefined + } + + if (session.timestamp < maxAge) { + session.destroy() + return undefined + } + + return session + } + + static getFromRequest(request: Request): Session | undefined { + const sessionId = getCookies(request.headers)['_SESSION'] ?? '' + return this.getSession(sessionId) + } + + private constructor() {} +} + +class Session { + #db = new Map() + #timestamp = Date.now() + #uuid = crypto.randomUUID() + #store: Map + + constructor(store: Map) { + this.#store = store + store.set(this.#uuid, this) + + //cleanup old sessions + const halfHour = 30 * 60 * 1_000 + const maxAge = this.#timestamp - halfHour + + for (const [uuid, session] of store.entries()) { + if (session.#timestamp < maxAge) { + store.delete(uuid) + } + } + } + + #updateTimestamp() { + this.#timestamp = Date.now() + } + + get uuid() { + return this.#uuid + } + + get timestamp() { + return this.#timestamp + } + + get(key: string): T | undefined { + this.#updateTimestamp() + + const entry = this.#db.get(key) + + // no entry + if (entry === undefined) return + + // flash entry + if (entry.flash) { + this.#db.delete(key) + return entry.value as T + } + + // normal entry + entry.accessCount++ + this.#db.set(key, entry) + return entry.value as T + } + set(key: string, value: T): void { + this.#updateTimestamp() + + const entry = this.#db.get(key) as SessionEntry | undefined + + // update or create + const newEntry: SessionEntry = { + accessCount: entry?.accessCount ? entry.accessCount++ : 0, + flash: entry?.flash ?? false, + value, + } + this.#db.set(key, newEntry) + } + delete(key: string): boolean { + this.#updateTimestamp() + + return this.#db.delete(key) + } + has(key: string): boolean { + this.#updateTimestamp() + + return this.#db.has(key) + } + list(): [key: string, value: unknown][] { + this.#updateTimestamp() + + const keys = [...this.#db.keys()] + return keys.map((key) => ([key, this.get(key)] as [string, unknown])) + } + flash(key: string, value: T) { + this.#updateTimestamp() + + const entry = this.#db.get(key) as SessionEntry | undefined + + // update or create + const newEntry: SessionEntry = { + accessCount: entry?.accessCount ? entry.accessCount++ : 0, + flash: true, + value, + } + this.#db.set(key, newEntry) + } + + destroy() { + this.#db.clear() + this.#store.delete(this.uuid) + } +} + +export type { Session }