99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import Redis, { type RedisOptions } from "ioredis";
|
|
import { defaultReconnectOnError } from "@internal/redis";
|
|
import { env } from "~/env.server";
|
|
import { subDays } from "date-fns";
|
|
|
|
const DEV_RECENT_DEBOUNCE_SEC = 60;
|
|
const DEV_RECENT_TTL = 7 * 24 * 60 * 60; // 7 days
|
|
const RECENCY_DAYS = 3;
|
|
|
|
export class DevPresence {
|
|
private redis: Redis;
|
|
|
|
constructor(options: RedisOptions) {
|
|
this.redis = new Redis({ reconnectOnError: defaultReconnectOnError, ...options });
|
|
}
|
|
|
|
async isConnected(environmentId: string) {
|
|
const presenceKey = this.getPresenceKey(environmentId);
|
|
const presenceValue = await this.redis.get(presenceKey);
|
|
return !!presenceValue;
|
|
}
|
|
|
|
async isConnectedMany(environmentIds: string[]): Promise<Map<string, boolean>> {
|
|
if (environmentIds.length === 0) return new Map();
|
|
const keys = environmentIds.map((id) => this.getPresenceKey(id));
|
|
const values = await this.redis.mget(keys);
|
|
return new Map(environmentIds.map((id, i) => [id, !!values[i]]));
|
|
}
|
|
|
|
async setConnected({
|
|
userId,
|
|
projectId,
|
|
environmentId,
|
|
ttl,
|
|
}: {
|
|
userId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
ttl: number;
|
|
}) {
|
|
const presenceKey = this.getPresenceKey(environmentId);
|
|
await this.redis.setex(presenceKey, ttl, new Date().toISOString());
|
|
|
|
const touchKey = this.getTouchKey(environmentId);
|
|
const acquired = await this.redis.set(touchKey, "1", "EX", DEV_RECENT_DEBOUNCE_SEC, "NX");
|
|
|
|
if (acquired !== null) {
|
|
const recentKey = this.getRecentKey(userId, projectId);
|
|
const now = new Date();
|
|
const threeDaysAgo = subDays(now, RECENCY_DAYS);
|
|
await this.redis
|
|
.multi()
|
|
.zadd(recentKey, now.getTime(), environmentId)
|
|
.zremrangebyscore(recentKey, 0, threeDaysAgo.getTime())
|
|
.zremrangebyrank(recentKey, 0, -51)
|
|
.expire(recentKey, DEV_RECENT_TTL)
|
|
.exec();
|
|
}
|
|
}
|
|
|
|
async getRecentBranchIds(userId: string, projectId: string) {
|
|
const recentKey = this.getRecentKey(userId, projectId);
|
|
const threeDaysAgo = subDays(Date.now(), RECENCY_DAYS);
|
|
const raw = await this.redis.zrevrangebyscore(
|
|
recentKey,
|
|
"+inf",
|
|
threeDaysAgo.getTime(),
|
|
"WITHSCORES"
|
|
);
|
|
|
|
const branches = new Map<string, Date>();
|
|
for (let i = 0; i < raw.length; i += 2) {
|
|
branches.set(raw[i], new Date(Number(raw[i + 1])));
|
|
}
|
|
return branches;
|
|
}
|
|
|
|
private getPresenceKey(environmentId: string) {
|
|
return `dev-presence:connection:${environmentId}`;
|
|
}
|
|
|
|
private getRecentKey(userId: string, projectId: string) {
|
|
return `dev-recent:${userId}:${projectId}`;
|
|
}
|
|
|
|
private getTouchKey(environmentId: string) {
|
|
return `dev-recent-touch:${environmentId}`;
|
|
}
|
|
}
|
|
|
|
export const devPresence = new DevPresence({
|
|
port: env.RUN_ENGINE_DEV_PRESENCE_REDIS_PORT ?? undefined,
|
|
host: env.RUN_ENGINE_DEV_PRESENCE_REDIS_HOST ?? undefined,
|
|
username: env.RUN_ENGINE_DEV_PRESENCE_REDIS_USERNAME ?? undefined,
|
|
password: env.RUN_ENGINE_DEV_PRESENCE_REDIS_PASSWORD ?? undefined,
|
|
enableAutoPipelining: true,
|
|
...(env.RUN_ENGINE_DEV_PRESENCE_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
|
|
});
|