Files
task-stream/libs/api/tracker/infrastructure/src/lib/redis/redis-presence.store.ts
Josep Piguillem f60d4c8346 feat: presence on tickets
A ticket's page beats every 10 s (touchPresence / PUT
/issues/{issue}/presence, with whether I am editing) and gets back who
else is there, editing ones first. Entries live in Redis, one hash per
ticket, and fade after 30 s without a beat; leaving the page takes me out
at once. Only for people who can read the ticket; never in the event
store. The ticket shows «Bob está editando · Ana también lo está viendo»
above its title; editing means a ticket or comment form is open or a
comment is being written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:53:47 +02:00

58 lines
1.6 KiB
TypeScript

import { Inject, Injectable } from '@nestjs/common';
import { REDIS_CLIENT } from '@task-stream/api/shared/infrastructure';
import {
PresenceEntry,
PresenceStore,
} from '@task-stream/api/tracker/application';
import Redis from 'ioredis';
/**
* One hash per ticket (field: user id, value: the entry). The hash expires
* when nobody beats on it; stale fields are skipped and dropped on reading.
*/
@Injectable()
export class RedisPresenceStore extends PresenceStore {
constructor(@Inject(REDIS_CLIENT) private readonly redis: Redis) {
super();
}
private key(issueId: string): string {
return `task-stream:presence:${issueId}`;
}
async touch(
issueId: string,
entry: PresenceEntry,
ttlMs: number,
): Promise<void> {
const key = this.key(issueId);
await this.redis
.multi()
.hset(key, entry.user.id, JSON.stringify(entry))
.pexpire(key, ttlMs)
.exec();
}
async list(issueId: string, since: number): Promise<PresenceEntry[]> {
const key = this.key(issueId);
const all = await this.redis.hgetall(key);
const fresh: PresenceEntry[] = [];
const stale: string[] = [];
for (const [userId, raw] of Object.entries(all)) {
try {
const entry = JSON.parse(raw) as PresenceEntry;
if (Date.parse(entry.at) >= since) fresh.push(entry);
else stale.push(userId);
} catch {
stale.push(userId);
}
}
if (stale.length) await this.redis.hdel(key, ...stale);
return fresh;
}
async leave(issueId: string, userId: string): Promise<void> {
await this.redis.hdel(this.key(issueId), userId);
}
}