north-star / commit
add stale friend cleanup
.vscode/settings.json | 4 ++++ database.rules.json | 2 +- src/app.ts | 34 ++++++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 98fc7a3..fd93652 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,10 @@ "/^npx tsc --noEmit && npm run build$/": { "approve": true, "matchCommandLine": true + }, + "/^npx tsc --noEmit && npm run build 2>&1 \\| tail -10$/": { + "approve": true, + "matchCommandLine": true } } } \ No newline at end of file diff --git a/database.rules.json b/database.rules.json index 85c888d..099c54b 100644 --- a/database.rules.json +++ b/database.rules.json @@ -7,7 +7,7 @@ ".read": "auth != null && $roomId.matches(/^[a-z0-9]{8}$/)", "friends": { "$uid": { - ".write": "auth != null && auth.uid === $uid", + ".write": "auth != null && (auth.uid === $uid || (!newData.exists() && data.child('lastSeen').isNumber() && data.child('lastSeen').val() < now - 604800000))", ".validate": "!newData.exists() || (newData.hasChildren(['encryptedState']) && newData.child('encryptedState').isString() && newData.child('encryptedState').val().length > 0 && !newData.child('id').exists() && !newData.child('name').exists() && !newData.child('color').exists() && !newData.child('position').exists() && !newData.child('timestamp').exists()) || (newData.hasChildren(['id','name','color','position','timestamp']) && newData.child('id').isString() && newData.child('id').val() === auth.uid && newData.child('name').isString() && newData.child('name').val().length > 0 && newData.child('name').val().length <= 32 && newData.child('color').isString() && newData.child('color').val().matches(/^#[0-9a-fA-F]{6}$/) && newData.child('position').hasChildren(['lat','lon']) && newData.child('position/lat').isNumber() && newData.child('position/lon').isNumber() && newData.child('timestamp').isNumber())" } } diff --git a/src/app.ts b/src/app.ts index 8fe447d..95253c6 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,4 @@ -import { ref, set } from 'firebase/database'; +import { get, ref, remove, set } from 'firebase/database'; import { getReadableTextColor, getArrowDisplayColor } from './utils'; import { encryptJson } from './crypto'; import { initDarkMode } from './darkmode'; @@ -37,6 +37,7 @@ const RENDER_INTERVAL_MS = 100; const LOCATION_SYNC_INTERVAL_MS = 3000; const FRIEND_ONLINE_GRACE_MS = 20_000; const DEFAULT_FRIEND_RECENTLY_OFFLINE_MS = 60 * 60 * 1000; +const STALE_FRIEND_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days const FRIEND_RECENTLY_OFFLINE_MS = (() => { const raw = Number(localStorage.getItem('friend-compass-recently-offline-ms')); if (Number.isFinite(raw) && raw > FRIEND_ONLINE_GRACE_MS) { @@ -95,10 +96,11 @@ const friendRef = ref(db, `rooms/${roomId}/friends/${uid}`); let selfFriend; const syncSelfFriend = async () => { - selfFriend.timestamp = Date.now(); + const now = Date.now(); + selfFriend.timestamp = now; const dbRecord = roomKey - ? { encryptedState: await encryptJson(roomKey, selfFriend) } - : selfFriend; + ? { encryptedState: await encryptJson(roomKey, selfFriend), lastSeen: now } + : { ...selfFriend, lastSeen: now }; try { await set(friendRef, dbRecord); } catch (err) { @@ -106,6 +108,29 @@ const syncSelfFriend = async () => { } }; +const cleanupStaleFriends = async () => { + try { + const roomFriendsRef = ref(db, `rooms/${roomId}/friends`); + const snapshot = await get(roomFriendsRef); + if (!snapshot.exists()) return; + const now = Date.now(); + const deletions: Promise<void>[] = []; + snapshot.forEach((child) => { + const val = child.val(); + const lastSeen = Number(val?.lastSeen); + if (Number.isFinite(lastSeen) && now - lastSeen > STALE_FRIEND_TTL_MS) { + deletions.push(remove(child.ref)); + } + }); + if (deletions.length > 0) { + console.debug(`Removing ${deletions.length} stale friend record(s)`); + await Promise.all(deletions); + } + } catch (err) { + console.error('Error cleaning up stale friends', err); + } +}; + let lastLocationSyncAt = 0; const maybeSyncLocation = () => { const now = Date.now(); @@ -209,4 +234,5 @@ const render = () => { }; await syncSelfFriend(); +void cleanupStaleFriends(); setInterval(render, RENDER_INTERVAL_MS);