sveltekit-sync
Version:
Local-first sync engine for SvelteKit
202 lines (201 loc) • 5.28 kB
JavaScript
/**
* 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();
}
}