2026-03-15 22:57:19 +00:00
|
|
|
import type { FeedSource } from "@aelis/core"
|
|
|
|
|
|
2026-03-05 02:01:30 +00:00
|
|
|
import type { FeedEnhancer } from "../enhancement/enhance-feed.ts"
|
2026-02-18 00:41:20 +00:00
|
|
|
import type { FeedSourceProviderInput } from "./feed-source-provider.ts"
|
2026-02-22 20:59:19 +00:00
|
|
|
|
2026-02-18 00:41:20 +00:00
|
|
|
import { UserSession } from "./user-session.ts"
|
|
|
|
|
|
2026-03-05 02:01:30 +00:00
|
|
|
export interface UserSessionManagerConfig {
|
|
|
|
|
providers: FeedSourceProviderInput[]
|
|
|
|
|
feedEnhancer?: FeedEnhancer | null
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 00:41:20 +00:00
|
|
|
export class UserSessionManager {
|
|
|
|
|
private sessions = new Map<string, UserSession>()
|
2026-03-15 22:57:19 +00:00
|
|
|
private pending = new Map<string, Promise<UserSession>>()
|
2026-02-18 00:41:20 +00:00
|
|
|
private readonly providers: FeedSourceProviderInput[]
|
2026-03-05 02:01:30 +00:00
|
|
|
private readonly feedEnhancer: FeedEnhancer | null
|
2026-02-18 00:41:20 +00:00
|
|
|
|
2026-03-05 02:01:30 +00:00
|
|
|
constructor(config: UserSessionManagerConfig) {
|
|
|
|
|
this.providers = config.providers
|
|
|
|
|
this.feedEnhancer = config.feedEnhancer ?? null
|
2026-02-18 00:41:20 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-15 22:57:19 +00:00
|
|
|
async getOrCreate(userId: string): Promise<UserSession> {
|
|
|
|
|
const existing = this.sessions.get(userId)
|
|
|
|
|
if (existing) return existing
|
|
|
|
|
|
|
|
|
|
const inflight = this.pending.get(userId)
|
|
|
|
|
if (inflight) return inflight
|
|
|
|
|
|
|
|
|
|
const promise = this.createSession(userId)
|
|
|
|
|
this.pending.set(userId, promise)
|
|
|
|
|
try {
|
|
|
|
|
const session = await promise
|
|
|
|
|
// If remove() was called while we were awaiting, it clears the
|
|
|
|
|
// pending entry. Detect that and destroy the session immediately.
|
|
|
|
|
if (!this.pending.has(userId)) {
|
|
|
|
|
session.destroy()
|
|
|
|
|
throw new Error(`Session for user ${userId} was removed during creation`)
|
|
|
|
|
}
|
2026-02-18 00:41:20 +00:00
|
|
|
this.sessions.set(userId, session)
|
2026-03-15 22:57:19 +00:00
|
|
|
return session
|
|
|
|
|
} finally {
|
|
|
|
|
this.pending.delete(userId)
|
2026-02-18 00:41:20 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
remove(userId: string): void {
|
|
|
|
|
const session = this.sessions.get(userId)
|
|
|
|
|
if (session) {
|
|
|
|
|
session.destroy()
|
|
|
|
|
this.sessions.delete(userId)
|
|
|
|
|
}
|
2026-03-15 22:57:19 +00:00
|
|
|
// Cancel any in-flight creation so getOrCreate won't store the session
|
|
|
|
|
this.pending.delete(userId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async createSession(userId: string): Promise<UserSession> {
|
|
|
|
|
const results = await Promise.allSettled(
|
|
|
|
|
this.providers.map((p) =>
|
|
|
|
|
typeof p === "function" ? p(userId) : p.feedSourceForUser(userId),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const sources: FeedSource[] = []
|
|
|
|
|
const errors: unknown[] = []
|
|
|
|
|
|
|
|
|
|
for (const result of results) {
|
|
|
|
|
if (result.status === "fulfilled") {
|
|
|
|
|
sources.push(result.value)
|
|
|
|
|
} else {
|
|
|
|
|
errors.push(result.reason)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sources.length === 0 && errors.length > 0) {
|
|
|
|
|
throw new AggregateError(errors, "All feed source providers failed")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const error of errors) {
|
|
|
|
|
console.error("[UserSessionManager] Feed source provider failed:", error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new UserSession(sources, this.feedEnhancer)
|
2026-02-18 00:41:20 +00:00
|
|
|
}
|
|
|
|
|
}
|