UNPKG

sveltekit-sync

Version:
202 lines (201 loc) 5.28 kB
/** * EphemeralStore - In-memory store for transient data like presence * * Data stored here is not persisted to database and expires after TTL. * Used for presence, awareness, and other real-time ephemeral data. */ /** * Generic in-memory store for ephemeral data */ export class EphemeralStore { store = new Map(); ttl; cleanupInterval = null; onExpire; constructor(options = {}) { this.ttl = options.ttl ?? 60000; // Default 60 seconds this.onExpire = options.onExpire; // Start automatic cleanup const interval = options.cleanupInterval ?? 30000; // Default 30 seconds if (interval > 0) { this.cleanupInterval = setInterval(() => { this.cleanup(); }, interval); } } /** * Store or update an entry */ set(key, entry) { const now = Date.now(); this.store.set(key, { ...entry, updatedAt: now, expiresAt: now + this.ttl, }); } /** * Get an entry by key * Returns null if not found or expired */ get(key) { const entry = this.store.get(key); if (!entry) return null; // Check if expired if (Date.now() > entry.expiresAt) { this.delete(key); return null; } return entry.data; } /** * Get full entry with metadata */ getEntry(key) { const entry = this.store.get(key); if (!entry) return null; // Check if expired if (Date.now() > entry.expiresAt) { this.delete(key); return null; } return entry; } /** * Delete an entry * Returns true if entry existed */ delete(key) { const entry = this.store.get(key); if (!entry) return false; this.store.delete(key); if (this.onExpire) { this.onExpire(entry); } return true; } /** * Get all entries for a specific channel */ getByChannel(channel) { const now = Date.now(); const entries = []; for (const entry of this.store.values()) { if (entry.channel === channel && entry.expiresAt > now) { entries.push(entry); } } return entries; } /** * Get all entries for a specific user across all channels */ getByUser(userId) { const now = Date.now(); const entries = []; for (const entry of this.store.values()) { if (entry.userId === userId && entry.expiresAt > now) { entries.push(entry); } } return entries; } /** * Get all data values in a channel (without metadata) * Optionally exclude a specific client */ getAllInChannel(channel, excludeClientId) { const now = Date.now(); const data = []; for (const entry of this.store.values()) { if (entry.channel === channel && entry.expiresAt > now && (!excludeClientId || entry.clientId !== excludeClientId)) { data.push(entry.data); } } return data; } /** * Get entry by channel and user */ getByChannelAndUser(channel, userId) { const now = Date.now(); for (const entry of this.store.values()) { if (entry.channel === channel && entry.userId === userId && entry.expiresAt > now) { return entry; } } return null; } /** * Remove all entries for a specific client */ removeByClient(clientId) { let removed = 0; for (const [key, entry] of this.store.entries()) { if (entry.clientId === clientId) { this.store.delete(key); removed++; } } return removed; } /** * Remove all entries in a channel */ removeByChannel(channel) { let removed = 0; for (const [key, entry] of this.store.entries()) { if (entry.channel === channel) { this.store.delete(key); removed++; } } return removed; } /** * Clean up expired entries * Called automatically on interval */ cleanup() { const now = Date.now(); const expired = []; for (const [key, entry] of this.store.entries()) { if (now > entry.expiresAt) { expired.push(key); if (this.onExpire) { this.onExpire(entry); } } } for (const key of expired) { this.store.delete(key); } } /** * Get the number of entries in the store */ size() { return this.store.size; } /** * Clear all entries */ clear() { this.store.clear(); } /** * Stop cleanup interval and clear store */ destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.clear(); } }