Files
buzz-sheet/lib/presence.ts
T
gunshiz 5aa0ae80c1
CI / Verify (push) Successful in 1m28s
CI / Build immutable images and deploy (push) Successful in 2m5s
feat : update
2026-09-05 18:15:06 +07:00

110 lines
3.0 KiB
TypeScript

import "server-only";
import type Redis from "ioredis";
import { getRedisClient } from "@/lib/redis/client";
import { PRESENCE_TTL_MS } from "@/lib/presence-constants";
type AdminPresence = {
userId: string;
name: string;
email: string;
guideId: string;
};
function prefix(): string {
return process.env.REDIS_PRESENCE_PREFIX || "buzz:presence";
}
async function purge(
redis: Redis,
activeKey: string,
detailKey: string,
now: number,
) {
const expired = await redis.zrangebyscore(activeKey, 0, now);
if (!expired.length) return;
await redis.multi().zrem(activeKey, ...expired).hdel(detailKey, ...expired).exec();
}
export async function updateVisitorPresence({
tabId,
guideId,
active,
}: {
tabId: string;
guideId: string | null;
active: boolean;
}) {
const redis = await getRedisClient();
const activeKey = `${prefix()}:visitors`;
const scopeKey = `${prefix()}:visitor-scopes`;
const now = Date.now();
if (active) {
await redis.multi()
.zadd(activeKey, now + PRESENCE_TTL_MS, tabId)
.hset(scopeKey, tabId, guideId ?? "")
.exec();
} else {
await redis.multi().zrem(activeKey, tabId).hdel(scopeKey, tabId).exec();
}
await purge(redis, activeKey, scopeKey, now);
const ids = await redis.zrangebyscore(activeKey, now + 1, "+inf");
const scopes = ids.length ? await redis.hmget(scopeKey, ...ids) : [];
return {
global: ids.length,
guide: guideId
? scopes.filter((scope) => scope === guideId).length
: null,
};
}
export async function updateAdminPresence({
memberId,
user,
guideId,
active,
}: {
memberId: string;
user: { id: string; name?: string | null; email: string };
guideId: string | null;
active: boolean;
}) {
const redis = await getRedisClient();
const activeKey = `${prefix()}:admins`;
const detailKey = `${prefix()}:admin-details`;
const now = Date.now();
if (active && guideId) {
const detail: AdminPresence = {
userId: user.id,
name: user.name?.trim() || user.email,
email: user.email,
guideId,
};
await redis.multi()
.zadd(activeKey, now + PRESENCE_TTL_MS, memberId)
.hset(detailKey, memberId, JSON.stringify(detail))
.exec();
} else {
await redis.multi().zrem(activeKey, memberId).hdel(detailKey, memberId).exec();
}
await purge(redis, activeKey, detailKey, now);
const ids = await redis.zrangebyscore(activeKey, now + 1, "+inf");
const serialized = ids.length ? await redis.hmget(detailKey, ...ids) : [];
const editorsByGuide: Record<string, Array<{ userId: string; name: string }>> = {};
for (const value of serialized) {
if (!value) continue;
try {
const detail = JSON.parse(value) as AdminPresence;
const editors = editorsByGuide[detail.guideId] ?? [];
if (!editors.some((editor) => editor.userId === detail.userId)) {
editors.push({ userId: detail.userId, name: detail.name || detail.email });
}
editorsByGuide[detail.guideId] = editors;
} catch {
// Ignore malformed ephemeral entries.
}
}
return { editorsByGuide };
}