Files
memind/plaza-redis.mjs
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

146 lines
4.6 KiB
JavaScript

import crypto from 'node:crypto';
const SYNC_SET = 'plaza:sync:post_ids';
const FEED_GEN_KEY = 'plaza:feed:gen';
function hashIp(ip) {
return crypto.createHash('sha256').update(String(ip ?? 'unknown')).digest('hex').slice(0, 16);
}
function counterKey(postId, field) {
return `plaza:post:${postId}:${field}`;
}
function viewDedupKey(postId, ipHash) {
return `plaza:view:${ipHash}:${postId}`;
}
function feedCacheKey(gen, sort, categorySlug, cursor) {
const category = categorySlug || 'all';
const page = cursor || 'root';
return `plaza:feed:${sort}:${category}:cursor:${page}:g${gen}`;
}
export function createNoopPlazaRedis(pool = null) {
const viewDedup = new Map();
return {
enabled: false,
async connect() {},
async disconnect() {},
async recordView(postId, ip) {
if (!pool) return { counted: false };
const ipHash = hashIp(ip);
const dedupKey = `${ipHash}:${postId}`;
if (viewDedup.has(dedupKey)) return { counted: false };
viewDedup.set(dedupKey, Date.now());
setTimeout(() => viewDedup.delete(dedupKey), 86_400_000).unref?.();
const now = Date.now();
const [result] = await pool.query(
`UPDATE plaza_posts SET view_count = view_count + 1, updated_at = ?
WHERE id = ? AND status = 'published'`,
[now, postId],
);
return { counted: (result.affectedRows ?? 0) > 0 };
},
async getFeedCache() {
return null;
},
async setFeedCache() {},
async invalidateFeedCaches() {},
async syncCountersToMySQL() {
return { synced: 0 };
},
async checkCommentRateLimit() {
return { allowed: true };
},
async markCommentRateLimit() {},
};
}
export async function createPlazaRedis(redisUrl, pool) {
if (!redisUrl) return createNoopPlazaRedis(pool);
const { createClient } = await import('redis');
const client = createClient({ url: redisUrl });
client.on('error', (error) => {
console.error('Plaza Redis error:', error.message);
});
await client.connect();
const getFeedGen = async () => Number((await client.get(FEED_GEN_KEY)) ?? 0);
return {
enabled: true,
client,
async disconnect() {
if (client.isOpen) await client.disconnect();
},
async recordView(postId, ip) {
const ipHash = hashIp(ip);
const dedup = viewDedupKey(postId, ipHash);
const inserted = await client.set(dedup, '1', { NX: true, EX: 86_400 });
if (!inserted) return { counted: false };
await client.incr(counterKey(postId, 'view_count'));
await client.sAdd(SYNC_SET, postId);
return { counted: true };
},
async getFeedCache(sort, categorySlug, cursor) {
const gen = await getFeedGen();
const key = feedCacheKey(gen, sort, categorySlug, cursor);
const raw = await client.get(key);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
},
async setFeedCache(sort, categorySlug, cursor, payload, ttlSeconds = 300) {
const gen = await getFeedGen();
const key = feedCacheKey(gen, sort, categorySlug, cursor);
await client.set(key, JSON.stringify(payload), { EX: ttlSeconds });
},
async invalidateFeedCaches() {
await client.incr(FEED_GEN_KEY);
},
async syncCountersToMySQL() {
const postIds = await client.sMembers(SYNC_SET);
if (postIds.length === 0) return { synced: 0 };
let synced = 0;
const now = Date.now();
for (const postId of postIds) {
const delta = Number((await client.get(counterKey(postId, 'view_count'))) ?? 0);
if (delta <= 0) {
await client.sRem(SYNC_SET, postId);
continue;
}
await pool.query(
`UPDATE plaza_posts SET view_count = view_count + ?, updated_at = ? WHERE id = ?`,
[delta, now, postId],
);
await client.decrBy(counterKey(postId, 'view_count'), delta);
const remaining = Number((await client.get(counterKey(postId, 'view_count'))) ?? 0);
if (remaining <= 0) {
await client.del(counterKey(postId, 'view_count'));
await client.sRem(SYNC_SET, postId);
}
synced += 1;
}
return { synced };
},
async checkCommentRateLimit(userId, postId, maxPerHour = 10) {
const key = `plaza:comment_rate:${userId}:${postId}`;
const count = Number((await client.get(key)) ?? 0);
return { allowed: count < maxPerHour, count };
},
async markCommentRateLimit(userId, postId) {
const key = `plaza:comment_rate:${userId}:${postId}`;
const count = await client.incr(key);
if (count === 1) await client.expire(key, 3600);
return count;
},
};
}